/* ==========================================================================
   The Learning Oasis — index, filters, episode page
   ========================================================================== */

/* The menubar offset lives in components.css, on .bl-has-nav, so every page
   that shows the menubar clears it — not just this one. */

/* --------------------------------------------------------------------------
   Head: title, search, filters
   -------------------------------------------------------------------------- */

.bl-oasis-head { padding-block: clamp(1.5rem, 1rem + 2vw, 3rem) var(--bl-space-7); }

.bl-breadcrumb {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--bl-space-2);
  margin-bottom: var(--bl-space-4);
  font-size: var(--bl-size-sm);
  font-weight: var(--bl-weight-medium);
  color: var(--bl-text-muted);
}

/* Falls back to the brand blue on the index, where no single category
   owns the page; on an episode --cat-ink is set on the article. */
.bl-breadcrumb a { color: var(--cat-fg, var(--cat-ink, var(--bl-blue))); }
.bl-breadcrumb a:hover { text-decoration: underline; text-underline-offset: 3px; }

.bl-oasis-head__title { margin-bottom: var(--bl-space-3); text-wrap: balance; }
.bl-oasis-head__standfirst { max-width: 46ch; }

/* Stacked by default. From the width where a 34rem search field and a 46ch
   standfirst can sit beside each other without either getting cramped, they
   do — which is also the width at which the stacked version started leaving
   an empty right half. Bottom-aligned so the field's baseline meets the
   standfirst rather than floating against the headline. */
.bl-oasis-head__top {
  display: grid;
  gap: var(--bl-space-6);
  margin-bottom: var(--bl-space-6);
}

@media (min-width: 60rem) {
  .bl-oasis-head__top {
    grid-template-columns: minmax(0, 1fr) minmax(19rem, 24rem);
    /* Centered on the intro block, which lands it beside the headline
       rather than trailing the standfirst — it reads as a peer of the
       title instead of a footnote to it. */
    align-items: center;
    column-gap: var(--bl-space-8);
  }
}

/* Search --------------------------------------------------------------- */

.bl-oasis-search {
  display: flex;
  align-items: center;
  gap: var(--bl-space-3);
  max-width: 34rem;
}

.bl-oasis-search .bl-field { flex: 1 1 auto; min-width: 0; }

.bl-oasis-search .bl-input {
  padding-block: 0.8125rem;
  box-shadow: 0 0 0 1px var(--bl-border);
}

/* Safari draws its own clear button on type=search; hide it so the field
   matches the rest of the form. */
.bl-oasis-search .bl-input::-webkit-search-decoration,
.bl-oasis-search .bl-input::-webkit-search-cancel-button { -webkit-appearance: none; }

.bl-oasis-search__submit { flex: none; padding-inline: 1.375rem; }

.bl-oasis-search__clear {
  flex: none;
  font-size: var(--bl-size-sm);
  font-weight: var(--bl-weight-bold);
  color: var(--bl-text-muted);
}

.bl-oasis-search__clear:hover { color: var(--bl-text); }

/* Filter swap ------------------------------------------------------------

   The two regions site.js replaces when a filter is clicked. They ease out
   together, the content is exchanged while they are invisible, and the new
   cards then arrive on their own staggered entrance. Slower on the way out
   than a crossfade would be, because this is the part the eye actually
   follows. */

[data-oasis-intro],
[data-oasis-results] {
  transition: opacity 260ms var(--bl-ease-out);
}

[data-oasis-intro].is-swapping,
[data-oasis-results].is-swapping {
  opacity: 0;
}

@media (prefers-reduced-motion: reduce) {
  [data-oasis-intro],
  [data-oasis-results] { transition: none; }
}

/* Chips ---------------------------------------------------------------- */

/* One line, always.
   Eight categories need ~1264px and the content column is ~1150px, so a
   wrapping row leaves a single orphan pill stranded on a second line — and
   on a phone it becomes five ragged rows before the first episode. Shrinking
   the type would fix the arithmetic and fail the audience: these readers are
   the reason the site exists and 13px labels are not for them.
   So the row scrolls instead. Every category is present at every width, the
   band always reads as one deliberate line, and the fade on the trailing
   edge says there is more without stealing any room. */
.bl-chips {
  display: flex;
  flex-wrap: nowrap;
  gap: var(--bl-space-2);
  overflow-x: auto;
  overscroll-behavior-x: contain;
  scroll-snap-type: x proximity;
  scrollbar-width: none;
  /* Bleed to the shell's edges so pills scroll off the page rather than
     out of an invisible box inset from it. */
  margin-inline: calc(var(--bl-gutter) * -1);
  padding-inline: var(--bl-gutter);
  /* A horizontal scroller clips vertically too, so the row has to reserve
     room for anything that paints outside a pill: the focus ring, the hover
     lift, and the selected pill's shadow — 0 2px 12px reaches 10px above and
     14px below. Three pixels only covered the focus ring, which is why the
     black "All" pill's shadow was being sliced off. The negative margins put
     the outer box back where it was, so nothing else moves. */
  padding-block: 12px 16px;
  margin-block: -9px -13px;
}

.bl-chips::-webkit-scrollbar { display: none; }

/* The fade is applied only when the row actually overflows, and drops away
   once you reach the end — a permanent fade over a row that already fits
   looks like a rendering fault. site.js sets both classes. */
.bl-chips.is-scrollable {
  -webkit-mask-image: linear-gradient(to right, #000 calc(100% - 4rem), transparent 100%);
  mask-image: linear-gradient(to right, #000 calc(100% - 4rem), transparent 100%);
}

.bl-chips.is-scrollable.is-scrolled {
  -webkit-mask-image: linear-gradient(to right, transparent 0, #000 2.5rem,
                              #000 calc(100% - 4rem), transparent 100%);
  mask-image: linear-gradient(to right, transparent 0, #000 2.5rem,
                              #000 calc(100% - 4rem), transparent 100%);
}

.bl-chips.is-scrollable.is-end {
  -webkit-mask-image: linear-gradient(to right, transparent 0, #000 2.5rem, #000 100%);
  mask-image: linear-gradient(to right, transparent 0, #000 2.5rem, #000 100%);
}

.bl-chip {
  display: inline-flex;
  flex: none;
  align-items: center;
  gap: 0.4375rem;
  padding: 0.5625rem 1rem;
  border: 1px solid var(--bl-border);
  border-radius: var(--bl-radius-pill);
  background: var(--bl-surface-raised);
  font-size: var(--bl-size-sm);
  font-weight: var(--bl-weight-bold);
  letter-spacing: -0.01em;
  color: var(--bl-text);
  white-space: nowrap;
  scroll-snap-align: start;
  scroll-margin-inline: var(--bl-gutter);
  transition: background-color var(--bl-dur-fast) var(--bl-ease-out),
              border-color var(--bl-dur-fast) var(--bl-ease-out),
              box-shadow var(--bl-dur-fast) var(--bl-ease-out),
              transform var(--bl-dur-fast) var(--bl-ease-out),
              color var(--bl-dur-fast) var(--bl-ease-out);
}

/* Hovering a pill previews its category's color, so the hover and the
   selected state agree. "All" owns no category and falls back to the brand
   blue. */
.bl-chip:hover {
  border-color: var(--cat-fg, var(--cat-ink, var(--bl-blue)));
  color: var(--cat-fg, var(--cat-ink, var(--bl-blue)));
  box-shadow: 0 2px 10px rgba(0, 0, 0, calc(0.07 * var(--bl-shadow-boost)));
  transform: translateY(-1px);
}

/* The selected pill wears its own category's accent, so the color you
   clicked is the color you now see on every card below. "All" has no
   category and keeps the neutral charcoal.

   The ink rather than the brand value, because this one carries white text.
   Brite Basics' cyan is #03d9dd — white on it is 1.75:1, which is not a
   legibility quibble, it is illegible. The ink is the same hue darkened only
   until white clears 4.5:1 on it, so the pill still reads as that category
   without the label dissolving into it. */
.bl-chip.is-active {
  background: var(--cat-ink, var(--bl-charcoal));
  border-color: var(--cat-ink, var(--bl-charcoal));
  color: var(--bl-white);
  box-shadow: 0 2px 12px rgba(0, 0, 0, calc(0.18 * var(--bl-shadow-boost)));
}

.bl-chip.is-active:hover { color: var(--bl-white); }

@media (prefers-reduced-motion: reduce) {
  .bl-chip:hover { transform: none; }
}

/* Brite Bytes is not a series, so a hairline separates it and "All" from the
   seven that are — air and a rule rather than a different pill shape, which
   would read as a different kind of control. The divider sits after this
   pill, between the two groups. */
.bl-chip--byte {
  position: relative;
  margin-inline-end: calc(var(--bl-space-2) + 0.75rem);
}

.bl-chip--byte::after {
  content: "";
  position: absolute;
  inset-block: 0.25rem;
  inset-inline-end: calc(-0.375rem - var(--bl-space-2) / 2);
  width: 1px;
  background: var(--bl-border);
}

.bl-chip__emoji { font-size: 0.9375em; line-height: 1; }

.bl-chip__icon { display: grid; place-items: center; width: 1.125rem; height: 1.125rem; }
.bl-chip__icon img { width: 100%; height: 100%; object-fit: contain; }
.bl-chip.is-active .bl-chip__icon img { filter: brightness(0) invert(1); }

/* Tag filter drawer ------------------------------------------------------ */

.bl-tagfilter { margin-top: var(--bl-space-4); }

.bl-tagfilter__summary {
  display: inline-flex;
  align-items: center;
  gap: var(--bl-space-2);
  padding: var(--bl-space-2) 0;
  font-size: var(--bl-size-sm);
  font-weight: var(--bl-weight-bold);
  color: var(--bl-text-muted);
  cursor: pointer;
  list-style: none;
}

.bl-tagfilter__summary::-webkit-details-marker { display: none; }
.bl-tagfilter__summary:hover { color: var(--bl-text); }

.bl-tagfilter__summary svg {
  width: 0.75rem;
  height: 0.75rem;
  transition: transform var(--bl-dur-base) var(--bl-ease-out);
}

.bl-tagfilter[open] .bl-tagfilter__summary svg { transform: rotate(180deg); }

.bl-chips--tags { margin-top: var(--bl-space-3); }

/* --------------------------------------------------------------------------
   Card grid
   -------------------------------------------------------------------------- */

.bl-oasis-list { padding-bottom: var(--bl-section-y); }

/* Featured lead ----------------------------------------------------------

   Three identical cards in a row is a list, not a page. Promoting the newest
   episode gives the eye somewhere to land first and gives the grid a reason
   to be a grid. It also fills the floor of a short page, which is what the
   Oasis looks like for its first few months. */

.bl-featured {
  position: relative;
  margin-bottom: clamp(2.5rem, 1.6rem + 2.8vw, 4rem);
  border-radius: var(--bl-radius-lg);
  overflow: hidden;
  background: var(--bl-surface-raised);
  border: 1px solid var(--bl-border);
  box-shadow: 0 1px 2px rgba(0, 0, 0, calc(0.04 * var(--bl-shadow-boost))), 0 12px 40px rgba(0, 0, 0, calc(0.05 * var(--bl-shadow-boost)));
  transition: box-shadow var(--bl-dur-base) var(--bl-ease-out),
              transform var(--bl-dur-base) var(--bl-ease-out);
}

.bl-featured:hover {
  transform: translateY(-2px);
  box-shadow: 0 1px 2px rgba(0, 0, 0, calc(0.05 * var(--bl-shadow-boost))), 0 18px 54px rgba(0, 0, 0, calc(0.09 * var(--bl-shadow-boost)));
}

.bl-featured__link { display: block; }

@media (min-width: 54rem) {
  .bl-featured__link {
    display: grid;
    grid-template-columns: 1.1fr 1fr;
    align-items: stretch;
  }
}

.bl-featured__media {
  position: relative;
  overflow: hidden;
  background: var(--bl-surface-sunken);
  aspect-ratio: 16 / 10;
}

/* Beside the text the art fills the column height instead of holding its own
   aspect ratio, so the two halves stay level however long the excerpt runs. */
@media (min-width: 54rem) {
  .bl-featured__media { aspect-ratio: auto; height: 100%; }
}

.bl-featured__media img,
.bl-featured__media .bl-cover {
  width: 100%;
  height: 100%;
  object-fit: cover;
  transition: transform var(--bl-dur-slow) var(--bl-ease-out);
}

.bl-featured:hover .bl-featured__media img,
.bl-featured:hover .bl-featured__media .bl-cover { transform: scale(1.03); }

/* The accent runs the full width of the card, not just the art. Stopping it
   at the column divide made it read as a stray edge on the image rather than
   a rule belonging to the whole block. */
.bl-featured::before {
  content: "";
  position: absolute;
  inset: 0 0 auto 0;
  z-index: 1;
  height: 4px;
  background: var(--cat, var(--bl-blue));
}

.bl-featured__body {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--bl-space-3);
  padding: clamp(1.5rem, 1rem + 2vw, 2.75rem);
}

.bl-featured__eyebrow {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  font-size: var(--bl-size-xs);
  font-weight: var(--bl-weight-bold);
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--cat-fg, var(--cat-ink, var(--bl-blue)));
}

/* A dot before the label, so the eyebrow carries the accent even when the
   words are short. */
.bl-featured__eyebrow::before {
  content: "";
  width: 0.5rem;
  height: 0.5rem;
  border-radius: 50%;
  background: currentColor;
}

.bl-featured__title {
  font-size: clamp(1.625rem, 1.2rem + 1.6vw, 2.375rem);
  font-weight: var(--bl-weight-black);
  line-height: 1.12;
  letter-spacing: var(--bl-track-display);
  text-wrap: balance;
}

.bl-featured__excerpt {
  font-size: var(--bl-size-body);
  line-height: 1.55;
  color: var(--bl-text-muted);
}

.bl-featured__meta {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem var(--bl-space-4);
  font-size: var(--bl-size-sm);
  color: var(--bl-text-muted);
}

.bl-featured__meta > * + *::before {
  content: "·";
  margin-right: var(--bl-space-4);
}

.bl-cards {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(21rem, 100%), 1fr));
  gap: clamp(2rem, 1.2rem + 2.4vw, 3.5rem) clamp(1.5rem, 1rem + 2vw, 2.75rem);
}

.bl-card__link {
  display: block;
  height: 100%;
  border-radius: var(--bl-radius-lg);
}

.bl-card__media {
  position: relative;
  margin-bottom: var(--bl-space-5);
  border-radius: var(--bl-radius-md);
  overflow: hidden;
  background: var(--bl-surface-sunken);
  aspect-ratio: 16 / 10;
}

.bl-card__media img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  transition: transform var(--bl-dur-slow) var(--bl-ease-out);
}

.bl-card__link:hover .bl-card__media img { transform: scale(1.035); }

.bl-card__placeholder {
  display: grid;
  place-items: center;
  width: 100%;
  height: 100%;
  opacity: 0.35;
}

.bl-card__badge {
  position: absolute;
  top: var(--bl-space-3);
  left: var(--bl-space-3);
  display: inline-flex;
  align-items: center;
  gap: 0.375rem;
  padding: 0.3125rem 0.6875rem;
  border-radius: var(--bl-radius-pill);
  background: rgba(255, 255, 255, 0.92);
  backdrop-filter: blur(8px);
  font-size: var(--bl-size-xs);
  font-weight: var(--bl-weight-bold);
  /* --cat is set per card in PHP from the category map; the brand blue is
     the fallback for an uncategorized entry. */
  color: var(--cat-fg, var(--cat-ink, var(--bl-blue)));
}

/* In the featured card the badge sits in the text flow rather than pinned
   over the art. This has to come after the base rule, not with the rest of
   the featured styles: the two selectors have equal specificity, so source
   order is the only thing deciding which position wins — and losing that
   race left the badge absolutely positioned against the page, floating off
   the side of the card. */
.bl-card__badge--inline {
  position: static;
  background: color-mix(in srgb, var(--cat, var(--bl-blue)) 12%, transparent);
  backdrop-filter: none;
}

/* The generated cover. An <svg> rather than an <img>, so it costs no request
   and can carry the category's color without a round trip. */
.bl-cover {
  display: block;
  width: 100%;
  height: 100%;
  transition: transform var(--bl-dur-slow) var(--bl-ease-out);
}

.bl-card__link:hover .bl-cover { transform: scale(1.035); }

/* The accent, stated once per card where it reads as a deliberate rule
   rather than decoration. */
.bl-card__media::after {
  content: "";
  position: absolute;
  inset: 0 0 auto 0;
  height: 3px;
  background: var(--cat, var(--bl-blue));
  opacity: 0.85;
}

/* Episode number and meta ------------------------------------------------

   The number sits in the type rather than over the artwork. A big ghosted
   numeral on the image looked handsome against a generated bloom and would
   have fought every real photograph uploaded later — and it wanted the same
   corner as the category badge. Here it costs one line, picks up the accent
   for free, and survives whatever the picture turns out to be. */

.bl-card__epnum {
  margin-bottom: var(--bl-space-2);
  font-size: var(--bl-size-xs);
  font-weight: var(--bl-weight-bold);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--cat-fg, var(--cat-ink, var(--bl-blue)));
  font-variant-numeric: tabular-nums;
}

.bl-card__meta {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.25rem var(--bl-space-3);
  margin-top: var(--bl-space-3);
  font-size: var(--bl-size-sm);
  color: var(--bl-text-muted);
}

/* A separating dot between whichever of the two are present, and never a
   leading one. */
.bl-card__meta > * + *::before {
  content: "·";
  margin-right: var(--bl-space-3);
}

.bl-card__title {
  margin-bottom: var(--bl-space-3);
  font-size: clamp(1.25rem, 1.05rem + 0.7vw, 1.5rem);
  font-weight: var(--bl-weight-bold);
  line-height: 1.25;
  letter-spacing: var(--bl-track-heading);
  text-wrap: balance;
}

.bl-card__excerpt {
  margin-bottom: var(--bl-space-3);
  font-size: var(--bl-size-sm);
  line-height: 1.55;
  color: var(--bl-text-muted);
}

/* The "Check out the episode" link only appears on hover in the reference
   design. Keyboard users get it on focus too, and it always occupies its
   space so nothing below shifts. */
.bl-card__cta {
  display: inline-flex;
  align-items: center;
  gap: 0.4375rem;
  font-size: var(--bl-size-sm);
  font-weight: var(--bl-weight-bold);
  color: var(--bl-blue);
  opacity: 0;
  transform: translateY(-2px);
  transition: opacity var(--bl-dur-base) var(--bl-ease-out),
              transform var(--bl-dur-base) var(--bl-ease-out);
}

.bl-card__cta svg {
  width: 0.875rem;
  height: 0.75rem;
  transition: transform var(--bl-dur-base) var(--bl-ease-out);
}

.bl-card__link:hover .bl-card__cta,
.bl-card__link:focus-visible .bl-card__cta { opacity: 1; transform: none; }
.bl-card__link:hover .bl-card__cta svg { transform: translateX(3px); }

/* Touch devices have no hover, so the link would never appear. */
@media (hover: none) {
  .bl-card__cta { opacity: 1; transform: none; }
}

/* The grid's CTA is hover-revealed because there are several of them and
   they would shout in chorus. There is only one featured block, and its CTA
   is the thing it is asking you to do — so it is always visible. After the
   base rule for the same reason the inline badge is: equal specificity, so
   whichever comes last wins. */
.bl-featured__cta {
  margin-top: var(--bl-space-2);
  opacity: 1;
  transform: none;
  /* The accent rather than the standard link blue: the whole card is one
     link, so this is a label for the block, not a link among prose. */
  color: var(--cat-fg, var(--cat-ink, var(--bl-blue)));
}

.bl-featured:hover .bl-featured__cta svg { transform: translateX(3px); }

/* Empty state ------------------------------------------------------------ */

/* Every category and device is listed whether or not it has episodes yet, so
   this is a page people reach on purpose rather than a dead end. Less air
   above it than before: the filter row is right there, and a screen of empty
   circuit board between the two reads as something failing to load. */
.bl-empty {
  max-width: 32rem;
  margin-inline: auto;
  padding-block: var(--bl-space-8) var(--bl-space-10);
  text-align: center;
}

.bl-empty__mark { margin: 0 auto var(--bl-space-5); opacity: 0.5; }
.bl-empty h2 { margin-bottom: var(--bl-space-3); }
.bl-empty p { margin-bottom: var(--bl-space-5); }
.bl-empty p:last-of-type { margin-bottom: var(--bl-space-6); }
.bl-empty a { color: var(--bl-blue); text-underline-offset: 0.18em; }

/* The "opening shortly" state only. An eyebrow rather than a second heading:
   the page title already says The Learning Oasis, and this names the room
   again without competing with the one line that matters. */
.bl-empty__eyebrow {
  margin-bottom: var(--bl-space-2) !important;
  font-size: var(--bl-size-sm);
  font-weight: var(--bl-weight-bold);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--bl-text-muted, var(--bl-text-on-light-muted));
}

/* Pagination ------------------------------------------------------------- */

.bl-pagination {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: var(--bl-space-4);
  margin-top: var(--bl-space-10);
}

.bl-pagination__pages { display: flex; gap: var(--bl-space-2); }

.bl-pagination__page,
.bl-pagination__step {
  display: grid;
  place-items: center;
  min-width: 2.5rem;
  height: 2.5rem;
  padding-inline: 0.75rem;
  border: 1px solid var(--bl-border);
  border-radius: var(--bl-radius-pill);
  background: var(--bl-surface-raised);
  font-size: var(--bl-size-sm);
  font-weight: var(--bl-weight-bold);
  transition: border-color var(--bl-dur-fast) var(--bl-ease-out),
              color var(--bl-dur-fast) var(--bl-ease-out);
}

.bl-pagination__page:hover,
.bl-pagination__step:hover { border-color: var(--bl-blue); color: var(--bl-blue); }

.bl-pagination__page.is-current {
  background: var(--bl-charcoal);
  border-color: var(--bl-charcoal);
  color: var(--bl-white);
}

.bl-pagination__step.is-disabled {
  opacity: 0.4;
  pointer-events: none;
}

.bl-pagination__gap {
  display: grid;
  place-items: center;
  min-width: 1.25rem;
  height: 2.5rem;
  color: var(--bl-text-muted);
}

/* --------------------------------------------------------------------------
   Episode page
   -------------------------------------------------------------------------- */

.bl-episode {
  position: relative;
  padding-block: clamp(1.5rem, 1rem + 2vw, 3rem) var(--bl-section-y);
}

/* The same 4px rule the card carried, continued onto the page it opens.
   Full bleed rather than inset: on the card it spanned the whole block, and
   an inset version here would read as a different device.

   ::after, not ::before, and this is load-bearing. The article also carries
   .bl-ground-circuit, whose tiled line art is painted on ITS ::before — and
   an element has only one of those. Both rules matched, specificity was
   equal, oasis.css loads second, and `background:` is a SHORTHAND, so this
   rule silently reset background-image to none and took the circuit off
   every episode page. Nothing looked broken; the texture was simply never
   there. The two effects are unrelated and now live on different
   pseudo-elements, which is the only arrangement where neither can erase
   the other. If a third decoration is ever needed here, it needs a real
   element — not a third pseudo that does not exist. */
.bl-episode::after {
  content: "";
  position: absolute;
  inset: 0 0 auto 0;
  height: 4px;
  background: var(--cat, var(--bl-blue));
}

.bl-episode .bl-shell { max-width: 56rem; }

.bl-episode__title { margin-bottom: var(--bl-space-4); text-wrap: balance; }

.bl-episode__subtitle {
  max-width: 54ch;
  margin-bottom: var(--bl-space-7);
  font-size: var(--bl-size-lead);
}

/* Meta strip ------------------------------------------------------------- */

/* Sized off the longest date of the year rather than by eye.
   "September 30, 2026" measures 129px in Satoshi at this size; with the
   cell's 40px of padding that is a 169px cell, and five of them need 845px.
   The article column is only 802px wide — which is why the date was breaking
   after the comma and leaving "2026" orphaned — but the shell around it is
   896px. So the strip alone steps out to the full shell width. It is the one
   element on the page that is a table rather than prose, and prose measure
   is not its constraint.

   The floor is 10.5rem (168px) so auto-fit lands on five tracks at 896px and
   on four when an entry has no episode number or no tags, with each track
   still comfortably clearing the date. */
.bl-meta {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(10.5rem, 100%), 1fr));
  margin: 0 0 var(--bl-space-7);
  margin-inline: calc(var(--bl-gutter) * -1);
  border: 1px solid var(--bl-border);
  border-radius: var(--bl-radius-md);
  background: var(--bl-surface-raised);
  overflow: hidden;
}

/* 20px inline rather than 24px: four pixels a side, times five cells, is
   40px — which is the difference between the date fitting and not. */
.bl-meta__cell { padding: var(--bl-space-4) 1.25rem; }

.bl-meta__cell + .bl-meta__cell { border-left: 1px solid var(--bl-border); }

.bl-meta__label {
  margin-bottom: 0.25rem;
  font-size: var(--bl-size-xs);
  font-weight: var(--bl-weight-bold);
  letter-spacing: 0.01em;
  color: var(--cat-fg, var(--cat-ink, var(--bl-blue)));
}

/* The date and the duration are single facts; they wrap into an orphan
   rather than reading as two lines. The track is sized to hold them. */
.bl-meta__value time,
.bl-meta__value--nowrap { white-space: nowrap; }

.bl-meta__value {
  margin: 0;
  font-size: var(--bl-size-sm);
  font-weight: var(--bl-weight-medium);
  line-height: 1.4;
}

.bl-meta__value a:hover { color: var(--bl-blue); }

.bl-meta__value--tags {
  display: flex;
  flex-wrap: wrap;
  gap: 0.25rem 0.5rem;
}

.bl-meta__value--tags a { color: var(--bl-text-muted); }
.bl-meta__value--tags a::after { content: ","; }
.bl-meta__value--tags a:last-child::after { content: ""; }

/* Five cells at 168px need 840px of strip, and the strip is as wide as the
   shell or the window, whichever is smaller. So below 860px there is no
   width at which five sit on one row, and every intermediate column count
   leaves a ragged last row: 4+1, or 3+2, or 2+2+1. A clean stack is the only
   arrangement that never strands a cell, so it starts here rather than
   fighting for two more columns. */
@media (max-width: 859px) {
  /* Stacked, so the strip returns inside the gutter — a full-bleed column
     would run to the edges of the screen. */
  .bl-meta {
    grid-template-columns: 1fr;
    margin-inline: 0;
  }

  .bl-meta__cell + .bl-meta__cell {
    border-left: 0;
    border-top: 1px solid var(--bl-border);
  }
}

/* Callouts --------------------------------------------------------------- */

.bl-callout {
  margin-block: var(--bl-space-6);
  padding: var(--bl-space-5) var(--bl-space-6);
  border-radius: var(--bl-radius-md);
  border: 1px solid transparent;
}

.bl-callout__title {
  margin-bottom: var(--bl-space-3);
  display: flex;
  align-items: center;
  gap: 0.5rem;
  font-size: var(--bl-size-body);
  font-weight: var(--bl-weight-bold);
  letter-spacing: -0.01em;
}

.bl-callout__body { font-size: var(--bl-size-sm); line-height: 1.6; }
.bl-callout__body > * + * { margin-top: var(--bl-space-3); }

.bl-callout--tldr,
.bl-callout--info {
  background: color-mix(in srgb, var(--cat, var(--bl-blue)) 7%, transparent);
  border-color: color-mix(in srgb, var(--cat, var(--bl-blue)) 22%, transparent);
}

.bl-callout--tldr .bl-callout__title,
.bl-callout--info .bl-callout__title { color: var(--cat-fg, var(--cat-ink, var(--bl-blue))); }

.bl-callout--tldr .bl-callout__body,
.bl-callout--info .bl-callout__body { color: var(--cat-fg, var(--cat-ink, #17607f)); }

/* The TL;DR block ---------------------------------------------------------

   The one element on the page that isn't the episode itself: a summary, so
   it behaves like something you can poke at. Hovering lifts it slightly and
   the label softens from the abbreviation to plain English. Scale is kept
   under 2% deliberately — anything more and the text re-rasterizes visibly,
   which reads as a glitch rather than a lift. */

/* Two-tone, matching the Framer mockup: a saturated band carrying the label
   over a much paler body. The alphas read straight off the reference —
   rgb(201,234,251) and rgb(235,251,254) over white are 21% and 8% — but the
   hue is now the episode's category rather than a fixed blue, so the Gist
   belongs to the page it sits on instead of being the one element that
   didn't get the memo. */
.bl-tldr {
  padding: 0;
  overflow: hidden;
  background: color-mix(in srgb, var(--cat, var(--bl-blue)) 8%, transparent);
  transition: transform 250ms var(--bl-ease-out),
              box-shadow 250ms var(--bl-ease-out),
              border-color 250ms var(--bl-ease-out);
}

.bl-tldr__title {
  align-items: center;
  margin: 0;
  padding: var(--bl-space-4) var(--bl-space-6);
  background: color-mix(in srgb, var(--cat, var(--bl-blue)) 21%, transparent);
}

.bl-tldr .bl-callout__body { padding: var(--bl-space-5) var(--bl-space-6); }

/* The growth is the point, not a flourish: at 1.1 the summary is a tenth
   larger in every direction, which is a real legibility gain for a reader
   who is squinting — and it is measured off the reference, where the box
   goes 709→782px wide and 208→229 tall.

   Scaled from the center, so it opens on both sides. A left origin made it
   creep rightwards across the page, which is not growth, it is drift.

   Guarded on a real pointer. :hover latches after a tap on a touchscreen,
   and a summary stuck at 110% with no way to dismiss it is a bug the reader
   cannot explain. */
@media (hover: hover) {
  .bl-tldr:hover {
    transform: scale(1.1);
    border-color: color-mix(in srgb, var(--cat, var(--bl-blue)) 40%, transparent);
    box-shadow: 0 10px 40px color-mix(in srgb, var(--cat, var(--bl-blue)) 22%, transparent);
  }
}

.bl-tldr__spark {
  display: grid;
  place-items: center;
  width: 1.125rem;
  height: 1.125rem;
  flex: none;
  transition: transform var(--bl-dur-slow) var(--bl-ease-spring);
}

.bl-tldr__spark svg { width: 100%; height: 100%; }

/* A quarter turn and a nudge in size: enough to feel alive, not enough to
   spin. Pointer only, like the lift above — the two were written apart and
   only the lift got the guard, so on a phone a tap latched :hover and you
   got the glyph turning and the label changing while the box itself sat
   still. Half a hover state is worse than none: it reads as something
   half-loaded rather than something responding. */
@media (hover: hover) {
  .bl-tldr:hover .bl-tldr__spark { transform: rotate(90deg) scale(1.12); }
}

/* Both labels occupy the same grid cell, so the box is as wide as the longer
   of the two and nothing shifts when they trade places. */
.bl-tldr__labels {
  display: inline-grid;
  align-items: center;
}

.bl-tldr__label {
  grid-area: 1 / 1;
  transition: opacity var(--bl-dur-base) var(--bl-ease-out),
              transform var(--bl-dur-base) var(--bl-ease-out);
}

.bl-tldr__label--hover {
  opacity: 0;
  transform: translateY(4px);
}

@media (hover: hover) {
  .bl-tldr:hover .bl-tldr__label--rest {
    opacity: 0;
    transform: translateY(-4px);
  }

  .bl-tldr:hover .bl-tldr__label--hover {
    opacity: 1;
    transform: none;
  }
}

/* Reduced motion takes away the animation, not the enlargement — the size
   is the accessibility feature here, and someone who asked for less motion
   may well be the same reader who needs the larger text. It snaps rather
   than eases. */
@media (prefers-reduced-motion: reduce) {
  .bl-tldr { transition: none; }
  .bl-tldr:hover .bl-tldr__spark { transform: none; }
  .bl-tldr__spark { transition: none; }
  .bl-tldr__label { transition: opacity var(--bl-dur-fast) linear; }
  .bl-tldr__label--hover,
  .bl-tldr:hover .bl-tldr__label--rest { transform: none; }
}

.bl-callout--tip {
  background: rgba(47, 158, 95, 0.08);
  border-color: rgba(47, 158, 95, 0.24);
}
.bl-callout--tip .bl-callout__title { color: #1f7347; }

.bl-callout--warning {
  background: rgba(217, 119, 59, 0.09);
  border-color: rgba(217, 119, 59, 0.26);
}
.bl-callout--warning .bl-callout__title { color: #9c4d16; }

/* Prose ------------------------------------------------------------------ */

.bl-prose { max-width: 68ch; }

.bl-prose > * + * { margin-top: var(--bl-space-5); }

.bl-prose__heading {
  margin-top: var(--bl-space-8);
  font-size: var(--bl-size-h3);
  font-weight: var(--bl-weight-bold);
  line-height: 1.25;
  letter-spacing: var(--bl-track-heading);
  scroll-margin-top: 7rem;
}

.bl-prose__heading:first-child { margin-top: 0; }

.bl-prose__text { font-size: var(--bl-size-body); line-height: 1.65; }
.bl-prose__text > * + * { margin-top: var(--bl-space-4); }

.bl-prose__text a {
  color: var(--bl-blue);
  text-decoration: underline;
  text-underline-offset: 2px;
  text-decoration-thickness: 1px;
}

.bl-prose__text a:hover { text-decoration-thickness: 2px; }

.bl-prose__text strong { font-weight: var(--bl-weight-bold); }

.bl-prose__text ul,
.bl-prose__text ol {
  padding-left: 1.25rem;
  list-style: disc;
}

.bl-prose__text ol { list-style: decimal; }
.bl-prose__text li + li { margin-top: var(--bl-space-3); }
.bl-prose__text li::marker { color: var(--bl-gray-400); }

.bl-prose__text code {
  padding: 0.125em 0.375em;
  border-radius: var(--bl-radius-xs);
  background: var(--bl-surface-sunken);
  font-size: 0.9em;
}

.bl-prose__divider {
  margin-block: var(--bl-space-8);
  border-top: 1px solid var(--bl-border);
}

/* A pull quote has to announce itself from across the room, because the one
   thing a reader must never do is mistake someone else's words for mine. A
   thin rule alone did not do that — it read as a paragraph that had been
   nudged. So: the brand blue rather than the yellow (which the site spends on
   calls to action), a tint of the same blue behind it so the block is a
   shape and not just an edge, real italics, and an oversized quotation mark
   doing the job punctuation was invented to do.

   Blue rather than the category accent on purpose. Everything else on this
   page takes its color from the category; the quote staying blue is what
   makes it recognizable as the same device on every episode. */
.bl-quote {
  position: relative;
  margin: var(--bl-space-8) 0;
  /* The left padding carries the mark, so it is measured from the mark's
     size rather than picked from the spacing scale. */
  padding: var(--bl-space-5) var(--bl-space-6) var(--bl-space-5) 4.25rem;
  border-left: 4px solid var(--bl-blue);
  border-radius: 0 var(--bl-radius-md) var(--bl-radius-md) 0;
  background: color-mix(in srgb, var(--bl-blue) 7%, transparent);
  font-size: var(--bl-size-lead);
  font-style: italic;
  font-weight: var(--bl-weight-medium);
  line-height: 1.45;
}

/* Upright and heavy: an italic quotation mark at this size reads as a smudge,
   and the glyph is doing graphic work here rather than typographic. */
.bl-quote::before {
  content: "\201C";
  position: absolute;
  left: var(--bl-space-5);
  top: 0.16em;
  font-size: 3.5rem;
  line-height: 1;
  font-style: normal;
  font-weight: var(--bl-weight-bold);
  color: var(--bl-blue);
  pointer-events: none;
}

.bl-quote p { margin: 0; }
.bl-quote p + p { margin-top: var(--bl-space-3); }

/* The attribution comes back upright and quiet — italics all the way down
   would blur the line between the quote and who said it. */
.bl-quote cite {
  display: block;
  margin-top: var(--bl-space-4);
  font-size: var(--bl-size-sm);
  font-style: normal;
  font-weight: var(--bl-weight-medium);
  color: var(--bl-text-muted);
}

@media (max-width: 34rem) {
  .bl-quote { padding: var(--bl-space-4) var(--bl-space-4) var(--bl-space-4) 3.25rem; }
  .bl-quote::before { left: var(--bl-space-3); font-size: 2.75rem; }
}

/* Share -------------------------------------------------------------------

   Sits under the body, above the newsletter call to action: the moment a
   reader has finished and is deciding whether this was worth passing on.
   Quiet by default — a rule and a row of plain targets, not a bank of brand
   colors, because five logos at the end of an article reads as advertising
   rather than as an offer to help.
   -------------------------------------------------------------------------- */

.bl-share {
  max-width: 68ch;
  margin-top: var(--bl-space-9);
  padding-top: var(--bl-space-6);
  border-top: 1px solid var(--bl-border);
}

.bl-share__title {
  margin: 0 0 var(--bl-space-4);
  font-size: var(--bl-size-body);
  font-weight: var(--bl-weight-bold);
}

.bl-share__row { display: flex; flex-wrap: wrap; gap: var(--bl-space-2); }

.bl-share__item {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.5625rem 0.9375rem;
  border: 1px solid var(--bl-border);
  border-radius: var(--bl-radius-pill);
  background: var(--bl-surface-raised);
  font-size: var(--bl-size-sm);
  font-weight: var(--bl-weight-bold);
  color: var(--bl-text-firm);
  text-decoration: none;
  cursor: pointer;
  transition: border-color var(--bl-dur-fast) var(--bl-ease-out),
              transform var(--bl-dur-fast) var(--bl-ease-out),
              box-shadow var(--bl-dur-fast) var(--bl-ease-out);
}

/* The category accent on hover, the same cue the filter pills use, so the
   whole site agrees on what "this one" looks like. */
.bl-share__item:hover {
  border-color: var(--cat-fg, var(--cat-ink, var(--bl-blue)));
  color: var(--cat-fg, var(--cat-ink, var(--bl-blue)));
  transform: translateY(-1px);
  box-shadow: 0 2px 10px rgba(0, 0, 0, calc(0.07 * var(--bl-shadow-boost)));
}

.bl-share__item:focus-visible { outline: 2px solid var(--bl-blue); outline-offset: 2px; }

.bl-share__item.is-done { border-color: var(--cat-fg, var(--cat-ink, var(--bl-blue))); color: var(--cat-fg, var(--cat-ink, var(--bl-blue))); }

.bl-share__icon { display: grid; place-items: center; width: 1.125rem; height: 1.125rem; }
.bl-share__icon svg { width: 100%; height: 100%; }

/* The phone version: one target, and a big one. This is the button someone
   presses with a thumb while holding the phone in one hand. */
.bl-share__sheet {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
}

@media (max-width: 34rem) {
  .bl-share__sheet { width: 100%; justify-content: center; }
}

@media (prefers-reduced-motion: reduce) {
  .bl-share__item:hover { transform: none; }
}

/* Preview banner ----------------------------------------------------------

   A preview is the live page in every other respect, which is the point and
   also the risk: nothing else on screen says this version exists only in the
   tab you are looking at. The strip sits at the foot rather than the head so
   it does not fight the menubar, and it is drawn from the body class alone —
   no markup in the view, which stays exactly what the public gets.
   -------------------------------------------------------------------------- */

body.bl-is-preview::after {
  /* \00a0 rather than a space: the space after \2014 is what terminates the
     escape, so it never reaches the page. */
  content: "Preview \2014\00a0this version is not saved";
  position: fixed;
  inset: auto 0 0 0;
  z-index: 9999;
  padding: 0.625rem var(--bl-gutter);
  padding-bottom: calc(0.625rem + env(safe-area-inset-bottom, 0px));
  background: var(--bl-charcoal);
  color: var(--bl-white);
  font-size: var(--bl-size-sm);
  font-weight: var(--bl-weight-bold);
  text-align: center;
  pointer-events: none;
}

/* Subscribe -------------------------------------------------------------- *

   A card, not a banner: it sits in the reading column at the weight of a
   callout so it never feels like an ad break in the middle of a lesson.
   The red is carried by the YouTube mark and the button, and nothing else,
   because a whole red panel in a page built from yellow, blue and charcoal
   reads as somebody else's content pasted in.
   -------------------------------------------------------------------------- */

.bl-subscribe {
  display: flex;
  align-items: center;
  gap: var(--bl-space-4);
  flex-wrap: wrap;
  margin: var(--bl-space-8) 0;
  padding: var(--bl-space-5);
  border: 1px solid var(--bl-border);
  border-radius: var(--bl-radius-lg);
  background: var(--bl-surface-raised);
  box-shadow: 0 1px 2px rgba(0, 0, 0, calc(0.04 * var(--bl-shadow-boost))), 0 10px 32px rgba(0, 0, 0, calc(0.04 * var(--bl-shadow-boost)));
}

.bl-subscribe__mark {
  flex: none;
  display: grid;
  place-items: center;
  width: 3rem;
  height: 3rem;
  border-radius: var(--bl-radius-md);
  background: color-mix(in srgb, #e60000 8%, transparent);
  color: #e60000;
}

.bl-subscribe__mark svg { width: 1.75rem; height: 1.25rem; }

.bl-subscribe__text { flex: 1 1 12rem; min-width: 0; }

.bl-subscribe__title {
  margin: 0;
  font-size: var(--bl-size-body);
  font-weight: var(--bl-weight-bold);
  line-height: 1.3;
}

.bl-subscribe__note {
  margin: 0.25rem 0 0;
  font-size: var(--bl-size-sm);
  color: var(--bl-text-muted);
  line-height: 1.45;
}

/* #e60000 rather than YouTube's #ff0000: white on pure red measures 4.0:1,
   which fails at this text size. This is 4.8:1 at rest and 5.9:1 on hover,
   and close enough to the red people are looking for. */
.bl-subscribe__btn {
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0.6875rem 1.375rem;
  border-radius: var(--bl-radius-pill);
  background: #e60000;
  color: var(--bl-white);
  font-weight: var(--bl-weight-bold);
  text-decoration: none;
  white-space: nowrap;
  transition: background-color var(--bl-dur-fast) var(--bl-ease-out),
              transform var(--bl-dur-fast) var(--bl-ease-out);
}

.bl-subscribe__btn:hover { background: #cc0000; transform: translateY(-1px); }
.bl-subscribe__btn:active { transform: none; }

.bl-subscribe__btn:focus-visible {
  outline: 2px solid var(--bl-blue);
  outline-offset: 3px;
}

/* Below the wrap, the button goes full width rather than sitting alone on a
   line at its natural size, which looks like it fell off the row. */
@media (max-width: 34rem) {
  .bl-subscribe__btn { flex: 1 1 100%; }
}

/* Figures ---------------------------------------------------------------- */

/* Media runs wider than the text measure so images have presence, but never
   wider than the shell. */
.bl-figure {
  margin: var(--bl-space-7) 0;
  width: min(100%, 56rem);
}

.bl-figure__grid { display: grid; gap: var(--bl-space-3); }
.bl-figure--2up .bl-figure__grid { grid-template-columns: 1fr 1fr; }
.bl-figure--3up .bl-figure__grid { grid-template-columns: repeat(3, 1fr); }

.bl-figure img {
  width: 100%;
  border-radius: var(--bl-radius-md);
  background: var(--bl-surface-sunken);
}

.bl-figure--2up img,
.bl-figure--3up img { aspect-ratio: 4 / 3; object-fit: cover; }

.bl-figure__caption {
  margin-top: var(--bl-space-3);
  /* Down from --bl-size-sm. A caption is an aside — mostly a photo credit —
     and at the old size it was competing with the body copy it sits above.
     0.75rem still clears 4.5:1 against the panel at 6.19:1, so smaller here
     costs nothing in legibility. */
  font-size: var(--bl-size-xs);
  color: var(--bl-text-muted);
}

/* Links inside a caption — almost always a photo credit.
   -------------------------------------------------------------------------
   No underline at rest, by request. Stated plainly so the next person does
   not "fix" it: this does not meet WCAG 1.4.1. The link is distinguished
   from the caption text by color alone, and that color is 2.00:1 against the
   muted gray in light mode and 1.13:1 in dark, where 3:1 is the floor.

   This was not a choice made without looking. The whole brand hue was walked
   from near-black to near-white against this exact caption color, and no
   value clears 3:1 against the gray while still clearing 4.5:1 against the
   panel — the gray sits in the middle of the range, and the best case
   anywhere on that ramp is 2.58:1 at a blue dark enough to read as body
   text. Color cannot carry this on its own, so there is no cleverer palette
   waiting to be found.

   The practical cost: on a phone, where there is no hover at all, nothing
   indicates the credit is tappable. If that is ever worth fixing, the
   cheapest honest fix is one line — font-weight on the rule below, which is
   a non-color cue and needs no underline.

   --bl-blue-deep rather than --bl-blue is kept: 4.85:1 against the panel
   versus 3.09:1, so the link text itself still passes as text. */
.bl-figure__caption a {
  color: var(--bl-blue-deep);
  text-decoration: none;
  text-decoration-thickness: 1px;
  text-underline-offset: 0.2em;
}

/* Focus gets what hover gets. A keyboard cannot hover, and this costs a
   pointer user nothing — :focus-visible does not fire on a mouse click. */
.bl-figure__caption a:hover,
.bl-figure__caption a:focus-visible {
  text-decoration: underline;
}

@media (max-width: 620px) {
  .bl-figure--2up .bl-figure__grid,
  .bl-figure--3up .bl-figure__grid { grid-template-columns: 1fr; }
}

.bl-episode__hero { margin-top: 0; }

/* Video facade ----------------------------------------------------------- */

.bl-video {
  position: relative;
  border-radius: var(--bl-radius-md);
  overflow: hidden;
  background: var(--bl-black);
  aspect-ratio: 16 / 9;
}

.bl-video__play {
  display: block;
  width: 100%;
  height: 100%;
  padding: 0;
  border: 0;
  background: none;
}

.bl-video__thumb {
  width: 100%;
  height: 100%;
  object-fit: cover;
  border-radius: 0;
  transition: transform var(--bl-dur-slow) var(--bl-ease-out),
              opacity var(--bl-dur-base) var(--bl-ease-out);
}

.bl-video__icon {
  position: absolute;
  inset: 50% auto auto 50%;
  translate: -50% -50%;
  display: grid;
  place-items: center;
  width: 4.5rem;
  height: 4.5rem;
  border-radius: 50%;
  background: var(--bl-yellow);
  color: var(--bl-black);
  box-shadow: var(--bl-shadow-lg);
  transition: transform var(--bl-dur-base) var(--bl-ease-spring);
}

.bl-video__icon svg { width: 1.75rem; height: 1.75rem; }

.bl-video__play:hover .bl-video__icon { transform: scale(1.08); }
.bl-video__play:hover .bl-video__thumb { opacity: 0.85; }

.bl-video iframe {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  border: 0;
}

/* Download --------------------------------------------------------------- */

.bl-download {
  display: flex;
  align-items: center;
  gap: var(--bl-space-4);
  margin-block: var(--bl-space-6);
  padding: var(--bl-space-4) var(--bl-space-5);
  border: 1px solid var(--bl-border);
  border-radius: var(--bl-radius-md);
  background: var(--bl-surface-raised);
  transition: border-color var(--bl-dur-fast) var(--bl-ease-out),
              box-shadow var(--bl-dur-fast) var(--bl-ease-out);
}

.bl-download:hover {
  border-color: var(--bl-blue);
  box-shadow: var(--bl-shadow-sm);
}

.bl-download__icon {
  display: grid;
  place-items: center;
  flex: none;
  width: 2.75rem;
  height: 2.75rem;
  border-radius: var(--bl-radius-sm);
  background: color-mix(in srgb, var(--cat, var(--bl-blue)) 10%, transparent);
  color: var(--cat-fg, var(--cat-ink, var(--bl-blue)));
}

.bl-download__icon svg { width: 1.375rem; height: 1.375rem; }

.bl-download__text { display: grid; gap: 0.125rem; }
.bl-download__title { font-weight: var(--bl-weight-bold); }
.bl-download__meta { font-size: var(--bl-size-sm); color: var(--bl-text-muted); }

/* --------------------------------------------------------------------------
   Newsletter CTA band
   -------------------------------------------------------------------------- */

.bl-oasis-cta { padding-block: clamp(3rem, 2rem + 4vw, 5rem); }

.bl-oasis-cta__inner {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--bl-space-6);
}

.bl-oasis-cta h2 { margin-bottom: var(--bl-space-3); }
.bl-oasis-cta p { max-width: 44ch; color: var(--bl-text-on-dark); }

/* --------------------------------------------------------------------------
   Related
   -------------------------------------------------------------------------- */

/* Matches .bl-episode .bl-shell so the rail's left edge lines up with the
   article above it rather than sitting 32px inboard of it. */
.bl-related { max-width: 56rem; }
.bl-related__title { margin-bottom: var(--bl-space-7); }
.bl-related__list > li + li { margin-top: var(--bl-space-5); }

.bl-related__item {
  display: grid;
  grid-template-columns: 8.75rem 1fr;
  gap: var(--bl-space-5);
  align-items: start;
}

.bl-related__media {
  display: block;
  border-radius: var(--bl-radius-sm);
  overflow: hidden;
  background: var(--bl-surface-sunken);
  aspect-ratio: 16 / 10;
}

.bl-related__media img,
.bl-related__media .bl-cover {
  width: 100%;
  height: 100%;
  object-fit: cover;
  transition: transform var(--bl-dur-slow) var(--bl-ease-out);
}

.bl-related__item:hover .bl-related__media img,
.bl-related__item:hover .bl-related__media .bl-cover { transform: scale(1.04); }

.bl-related__text { display: grid; gap: 0.375rem; }

.bl-related__heading {
  font-size: clamp(1.125rem, 1rem + 0.5vw, 1.375rem);
  font-weight: var(--bl-weight-bold);
  line-height: 1.25;
  letter-spacing: var(--bl-track-heading);
}

.bl-related__item:hover .bl-related__heading { color: var(--bl-blue); }

.bl-related__excerpt {
  font-size: var(--bl-size-sm);
  line-height: 1.5;
  color: var(--bl-text-muted);
}

@media (max-width: 560px) {
  .bl-related__item { grid-template-columns: 5.5rem 1fr; gap: var(--bl-space-4); }
}

/* --------------------------------------------------------------------------
   404
   -------------------------------------------------------------------------- */

.bl-notfound {
  /* Wider than the old 40rem, because four buttons on one line need the
     room. The note itself is height-capped and centers inside whatever this
     is, so widening the container does not widen the drawing. */
  max-width: 56rem;
  margin-inline: auto;
  text-align: center;
}

/* The note.

   A tall portrait image carrying the whole message, so it is capped by
   HEIGHT rather than width: capping the width on a 3:4 picture means a
   laptop gets a page you have to scroll to read, which defeats the point of
   a 404. 62vh leaves the links comfortably above the fold on a 13-inch
   screen and still gives the drawing room to be the thing you notice. The
   aspect-ratio matches the file exactly, so the box is reserved before the
   bytes arrive and the links never jump out from under a cursor. */
.bl-notfound__note {
  display: block;
  width: auto;
  max-width: 100%;
  height: auto;
  max-height: 62vh;
  aspect-ratio: 1086 / 1448;
  /* The file has a transparent background and torn edges that are meant to
     sit on the page itself, so: no background, no radius, nothing behind
     it. The circuit ground shows through the tear, which is the point. */
  background: none;
  margin: 0 auto var(--bl-space-5);
}

/* The note is tall, so the usual section padding pushes it below the fold
   before it has said anything. Pulled up toward the menubar instead. */
.bl-404 .bl-section--tight {
  padding-block-start: var(--bl-space-5);
}

/* The repeated sentences. Quieter than the drawing above them — they are a
   second reading of the same message, not a second message — but full-size
   and real, which is the entire point of their being here. */
.bl-notfound__say {
  max-width: 34rem;
  margin: 0 auto var(--bl-space-6);
}

.bl-notfound__body { margin-bottom: var(--bl-space-2); }

.bl-notfound__prompt {
  margin: 0;
  /* --bl-accent-text, not --bl-teal. The teal in the drawing measures 4.02:1
     on white, which is under the 4.5:1 floor for normal text — it is fine as
     ink in a picture and not fine as live type. This token is the one already
     built to flip with the theme: 7.63:1 on light, 5.92:1 on dark. */
  color: var(--bl-accent-text);
  font-weight: var(--bl-weight-medium);
}

/* One row on desktop, as asked. nowrap would push them off a narrow screen,
   so the wrap comes back below the breakpoint rather than never — four
   buttons side by side at 390px would be four slivers of unreadable text.
   The shrink-to-zero default is what actually breaks a single row, so the
   items are told not to. */
.bl-notfound__links {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: var(--bl-space-3);
}

@media (min-width: 48rem) {
  .bl-notfound__links { flex-wrap: nowrap; }
  .bl-notfound__links > .bl-btn { flex: 0 0 auto; white-space: nowrap; }
}

/* --------------------------------------------------------------------------
   Featured carousel

   Shown only when more than one entry is flagged Featured. One flagged entry
   renders as the plain lead card; none, and the newest entry leads instead.

   Built as a scroll-snap rail, not a transform track. Three things fall out
   of that for free: it works with no JavaScript at all, a trackpad or a
   thumb swipes it natively, and the browser handles the momentum — which no
   hand-written easing has ever quite matched.
   -------------------------------------------------------------------------- */

/* The gap between slides is sized from the card's shadow, not from the
   spacing scale. The shadow is 0 18px 54px on hover, which paints about 27px
   to each side; at the old 24px gap the next card's opaque white body landed
   inside that and sliced the shadow off in a straight vertical line. Anything
   wider than the shadow's reach lets it fade out into the page the way it
   does everywhere else on the site. */
.bl-carousel {
  position: relative;
  --bl-carousel-gap: 2rem;

  /* How far the rail reaches past the shell on each side.

     Written from the viewport rather than as a percentage on purpose. The
     obvious spelling is calc(50vw - 50%), and it is correct in margin and
     padding — but the same token is reused in a mask gradient, where a
     percentage resolves against the mask's own box instead of the shell, and
     50vw - 50% quietly evaluates to zero. Viewport units mean one expression
     that means the same thing everywhere it is used.

     Reading it: half of whatever the shell is not using, plus the gutter,
     which is the distance from the shell's text edge to the page edge. Below
     the shell's max-width that is just the gutter, so this collapses to the
     layout it always had and no breakpoint is needed.

     Capped at 16rem because the card stops growing at --bl-max-width while
     the page does not: uncapped, a 2560px monitor shows 716px of neighbour
     beside a 1128px card, which is more waiting room than card. The cap
     bites around 1640px, and the fade below hides where the rail stops, so
     it still reads as running off the page. */
  --bl-carousel-bleed:
    min(calc((100vw - min(var(--bl-max-width), 100vw)) / 2 + var(--bl-gutter)), 16rem);
  /* Deliberately larger than the gap above the controls. The controls used to
     sit 32px under the card and 40px above the grid, so the eye filed them
     with the grid — which is exactly what they do not control. */
  margin-bottom: clamp(3.5rem, 2.4rem + 3.4vw, 5.5rem);
}

.bl-carousel__rail {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: 100%;
  gap: var(--bl-carousel-gap);
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  scrollbar-width: none;

  /* Edge to edge, the way Apple runs theirs: the rail spans the page while
     the card stays at the reading width, so the neighbours have real room to
     sit in rather than a sliver of gutter. --bl-carousel-bleed is how far
     past the shell the rail reaches; the negative margin goes out by it and
     the matching padding puts the card back exactly where it was.

     body has overflow-x: hidden, so the viewport units here cannot open a
     horizontal scrollbar on a machine that shows one. */
  margin-inline: calc(var(--bl-carousel-bleed) * -1);
  padding-inline: var(--bl-carousel-bleed);
  /* Room for the card's own shadow, which a scroll container clips on all
     four sides. The hover shadow sits 18px down with a 54px blur, so it
     reaches ~9px above the card and ~45px below it — the old 14px bottom cut
     it off mid-fade, which is the hard horizontal line under the card. */
  padding-block: 12px 48px;
  margin-block: -12px -48px;
}

.bl-carousel__rail::-webkit-scrollbar { display: none; }

/* Edge fades -------------------------------------------------------------

   The peeking neighbour is deliberate, but cut off square at the rail's edge
   it reads as a card that failed to render rather than one waiting its turn.

   The fade is the bleed: the same distance the rail reaches past the shell,
   so it covers precisely the region outside the card and stops where the
   card begins, at every width and with no breakpoint. site.js sets the three
   classes, the same ones the filter pills use — which is what keeps the fade
   off the side you cannot scroll towards, and so off the current card's
   shadow at either end of the rail. */

.bl-carousel__rail { --bl-carousel-fade: var(--bl-carousel-bleed); }

.bl-carousel__rail.is-scrollable {
  -webkit-mask-image: linear-gradient(to right, #000 calc(100% - var(--bl-carousel-fade)), transparent 100%);
  mask-image: linear-gradient(to right, #000 calc(100% - var(--bl-carousel-fade)), transparent 100%);
}

.bl-carousel__rail.is-scrollable.is-scrolled {
  -webkit-mask-image: linear-gradient(to right, transparent 0, #000 var(--bl-carousel-fade),
                                      #000 calc(100% - var(--bl-carousel-fade)), transparent 100%);
  mask-image: linear-gradient(to right, transparent 0, #000 var(--bl-carousel-fade),
                              #000 calc(100% - var(--bl-carousel-fade)), transparent 100%);
}

.bl-carousel__rail.is-scrollable.is-end {
  -webkit-mask-image: linear-gradient(to right, transparent 0, #000 var(--bl-carousel-fade), #000 100%);
  mask-image: linear-gradient(to right, transparent 0, #000 var(--bl-carousel-fade), #000 100%);
}

/* 100% of the rail's content box, which the padding above has already set to
   the shell's width. So the card is exactly as wide as the text it sits
   above, and the peek comes from the bleed rather than from shrinking the
   card to make room for it — which is why there is no longer a second width
   to keep in step with the gap. */

.bl-carousel__slide { scroll-snap-align: center; min-width: 0; }

/* The card fills its slide and keeps its own styling. Its standalone bottom
   margin belongs to a card sitting alone above the grid, not to one inside a
   rail that is already spacing itself. */
.bl-carousel__slide .bl-featured { height: 100%; margin-bottom: 0; }

/* Controls ----------------------------------------------------------------

   One row, two controls, both the same height so they read as a pair rather
   than as a thing and another thing. 28px each: big enough to hit without
   looking, small enough that it stays a footnote to the card above it.
   -------------------------------------------------------------------------- */

.bl-carousel__controls {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.5rem;
  /* Close enough to the card to be read as part of it. The rail's negative
     bottom margin cancels its own shadow padding, so this number is the
     actual gap you see between the card and the pills. */
  margin-top: var(--bl-space-5);
}

.bl-carousel__dots,
.bl-carousel__toggle {
  height: 1.75rem;
  border-radius: var(--bl-radius-pill);
  background: var(--bl-surface-sunken);
}

.bl-carousel__dots {
  display: flex;
  align-items: center;
  gap: 0.125rem;
  padding-inline: 0.5rem;
}

/* A dot is a hit target first and a marker second: 5px of ink inside a box
   big enough to press without aiming, because the audience is not
   twenty-five and these are deliberately small. */
.bl-carousel__dot {
  position: relative;
  width: 1rem;
  height: 1.75rem;
  padding: 0;
  border: 0;
  background: none;
  cursor: pointer;
  display: grid;
  place-items: center;
}

.bl-carousel__dot::before {
  content: "";
  width: 5px;
  height: 5px;
  border-radius: 50%;
  background: var(--bl-text-muted);
  opacity: 0.5;
  transition: opacity var(--bl-dur-fast) var(--bl-ease-out);
}

.bl-carousel__dot:hover::before { opacity: 0.85; }

/* The current one stretches into a track and the fill inside it runs with
   the timer, so the control answers "how long until this moves?" without
   anyone having to watch for it. */
.bl-carousel__dot[aria-selected="true"] { width: 1.75rem; }

.bl-carousel__dot[aria-selected="true"]::before {
  width: 100%;
  height: 5px;
  border-radius: var(--bl-radius-pill);
  background: color-mix(in srgb, var(--bl-charcoal) 22%, transparent);
  opacity: 1;
}

/* The running bar wears the current card's category color — the same color
   painting the 4px rule across the top of that card. It is the second half of
   the answer to "which thing do these buttons drive": proximity puts them
   under the cards, and this makes the active one change color in step with
   the card it is pointing at. Falls back to charcoal for a card with no
   category, where there is no accent to borrow.

   --cat-ink rather than --cat: the brand value is tuned for a 4px rule on
   white, and the brightest of them (the cyan) all but vanishes as a 5px bar
   on the grey pill. The ink is the same hue at a weight you can see. */
.bl-carousel__fill {
  position: absolute;
  left: 0;
  height: 5px;
  width: 0;
  border-radius: var(--bl-radius-pill);
  background: var(--cat-ink, var(--bl-charcoal));
  pointer-events: none;
  transition: background-color var(--bl-dur-base) var(--bl-ease-out);
}

.bl-carousel__dot[aria-selected="true"] .bl-carousel__fill { width: var(--bl-progress, 0%); }

.bl-carousel__toggle {
  display: grid;
  place-items: center;
  width: 1.75rem;
  padding: 0;
  border: 0;
  color: var(--bl-text-firm);
  cursor: pointer;
  transition: background-color var(--bl-dur-fast) var(--bl-ease-out);
}

.bl-carousel__toggle:hover { background: var(--bl-gray-300); }
.bl-carousel__toggle svg { width: 0.625rem; height: 0.625rem; }

.bl-carousel__toggle .bl-carousel__play { display: none; }
.bl-carousel__toggle[data-paused="true"] .bl-carousel__pause { display: none; }
.bl-carousel__toggle[data-paused="true"] .bl-carousel__play { display: block; }

.bl-carousel__dot:focus-visible,
.bl-carousel__toggle:focus-visible {
  outline: 2px solid var(--bl-blue);
  outline-offset: 2px;
}

/* Nothing advances on its own for a reader who has asked for less motion.
   The dots still work; only the timer goes away. */
@media (prefers-reduced-motion: reduce) {
  .bl-carousel__rail { scroll-behavior: auto; }
  .bl-carousel__fill { display: none; }
}
