/* ============================================================
   JJ Community — public pages
   ============================================================
   Tokens copied from the marketing site's @theme block verbatim, not
   approximated, so the community section and the main site read as one
   product rather than two that merely resemble each other.

   Teal is the material. Gold is the assay mark — it appears only where
   something is certified, measured, dated or counted, and never above
   about 10% of a section's surface.
   ============================================================ */

:root {
  /* Scale anchored on the flagship logo teal (#034848). */
  --jj-900: #022A2A;
  --jj-700: #023B3B;
  --jj-500: #034848;
  --jj-300: #46A09B;
  --jj-100: #D7E2E2;

  --assay:      #C9A227;   /* gold — DARK grounds only */
  --assay-deep: #836919;   /* gold — LIGHT grounds. #C9A227 is 2.24:1 here */

  --showcase: #F4F7F5;     /* light ground. Cool green undertone, not cream */
  --ink:      #0B1F1F;

  /* Ground-aware. Never hardcode either — .on-dark flips both. */
  --gold:       var(--assay-deep);
  --focus-ring: var(--jj-500);

  --hairline:     1px solid color-mix(in oklab, var(--jj-100) 25%, transparent);
  --hairline-ink: 1px solid color-mix(in oklab, var(--ink) 14%, transparent);

  /* EDGE TO EDGE. The shell has no max-width — content tracks the viewport
     and only the gutter holds it off the edge. Blocks that need a readable
     line length cap themselves instead.
       360px -> 20px · 1440px -> 58px · 1920px -> 77px · 2560px -> 96px */
  --gutter: clamp(1.25rem, 4vw, 6rem);
  /* THE TOP OF A PAGE IS NOT A GAP BETWEEN SECTIONS.
     --section-pad is the rhythm BETWEEN blocks — it exists to separate
     something that ended from something that starts. The first element
     under the header has nothing above it to be separated from, so
     spending the full section rhythm there just pushes the content down:
     measured at 101px between the navbar and the eyebrow, on a page whose
     lead image then starts below the fold. Roughly half is enough to stop
     the title colliding with the bar. */
  --page-top: clamp(1.75rem, 3.5vw, 3rem);

  --section-pad: clamp(3.5rem, 7vw, 6.5rem);
  --measure: 68ch;

  /* THE ARTICLE'S WIDTH. Site-level because the comments block sits on
     the same measure and used to carry its own copy of the number — two
     constants that must agree are one constant somebody edits alone.

     ONE WIDTH, NOT TWO. The text and the cover were 44rem and 56rem, so
     every paragraph started 96px inside the picture above it and the
     left edge stepped in and out down the page. They are now the same
     number, deliberately, and --art-measure is defined FROM --art-wide
     so the two cannot drift apart again. See "The article page" below
     for what it cost. */
  --art-wide: 56rem;
  --art-measure: var(--art-wide);

  /* Type. Zodiak for headings, Satoshi for everything else — both
     self-hosted, see fonts.css. Zodiak is a high-contrast serif: thick
     and thin, which is what gold actually looks like when light catches
     an edge. It also puts real distance between this section and the
     marketing site's flat Archivo. */
  /* MUKTA IS IN THE DISPLAY STACK TOO, AND IT HAS TO BE. Zodiak has no
     Devanagari, and neither Georgia nor Times fills the gap — a Nepali
     heading fell through to whatever the system offered, or to tofu.
     Now that the interface can be Nepali, every h1 on the site depends
     on this line.

     It is a compromise and worth naming: Zodiak is a high-contrast
     serif and Mukta is a humanist sans, so a Nepali headline does not
     have the same voice as an English one. There is no Devanagari
     companion to Zodiak. Mukta is the same face the marketing site uses
     for Nepali, so at least the two halves of the product agree. */
  /* Martel, not Mukta, for the Devanagari half — Mukta is a sans and
     put Nepali headlines in a different kind of typeface from the
     English ones. See the note in fonts.css. */
  --font-display: 'Zodiak', 'Martel', Georgia, 'Times New Roman', serif;
  --font-body: 'Satoshi', 'Mukta', system-ui, -apple-system, 'Segoe UI', sans-serif;

  --text-display-l: clamp(2.25rem, 5.5vw, 4rem);
  --text-display-m: clamp(1.75rem, 3.5vw, 2.5rem);
  --text-display-s: clamp(1.375rem, 2.4vw, 1.75rem);
  --text-body-l: 1.125rem;
  --text-eyebrow: 0.75rem;

  --radius: 3px;
}

/* Inside a dark band gold becomes the bright mark and the focus ring lifts
   to jj-300. Lifted straight from the marketing site's .on-dark. */
.on-dark {
  color: var(--jj-100);
  --hairline-ink: var(--hairline);
  --gold: var(--assay);
  --focus-ring: var(--jj-300);
}

* { box-sizing: border-box; }

body {
  margin: 0;
  background: var(--showcase);
  color: var(--ink);
  font-family: var(--font-body);
  /* 17px, not 16. 16 is the floor below which text gets flagged as too
     small; 17–18 measurably improves long-form reading with no downside,
     and this section is mostly long-form. */
  font-size: 17px;
  /* 1.6–1.7 is the band for articles. 1.5 is a UI number. */
  line-height: 1.65;
  -webkit-font-smoothing: antialiased;
}

/* The shell. NO max-width, by design — see --gutter above. */
.wrap {
  width: 100%;
  margin-inline: auto;
  padding-inline: var(--gutter);
}

/* For blocks that must NOT run the full width even though their section
   does. A full-bleed ground with a sane content width inside it. */
.narrow { width: 100%; max-width: 84rem; margin-inline: auto; }

.display {
  font-family: var(--font-display);
  font-weight: 800;
  letter-spacing: -0.018em;
  text-wrap: balance;
}
/* Figures stay in the body face with tabular numerals rather than a mono.
   Zodiak's high contrast is lovely at headline size and noisy at 12px. */
.num { font-variant-numeric: tabular-nums; }

a { color: var(--jj-500); }
.on-dark a { color: var(--jj-300); }
a:focus-visible, button:focus-visible {
  outline: 2px solid var(--focus-ring);
  outline-offset: 2px;
}

/* Gold underline under one word of a headline. Sized in em so it tracks the
   type, drawn as a background so it never affects layout. */
.rule-gold {
  background-image: linear-gradient(var(--gold), var(--gold));
  background-repeat: no-repeat;
  background-position: 0 100%;
  background-size: 100% 0.075em;
  padding-bottom: 0.08em;
}

/* ── Chrome ────────────────────────────────────────────────────────── */
/* Opaque, like the marketing nav. A translucent blurred bar over a dark
   band blends to muddy grey. */
.site-head {
  background: var(--showcase);
  border-bottom: var(--hairline-ink);
  position: sticky;
  top: 0;
  z-index: 50;
}
.head-inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1.5rem;
  height: 4.0625rem;   /* 65px — the marketing header's height */
}
.brand { text-decoration: none; color: var(--ink); line-height: 1.15; }
.brand strong {
  display: block;
  font-family: var(--font-display);
  font-weight: 700;
  font-size: 1.05rem;
  letter-spacing: -0.01em;
}
.brand span {
  display: block;
  font-family: var(--font-body);
  font-size: 0.6rem;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--gold);
}
/* ── Language ────────────────────────────────────────────────────────
   Two links, not a select — see the note in the layout. Sized and
   spaced as one control so the pair reads as a switch rather than as
   two more nav items. */
.langpick {
  display: flex;
  flex: none;
  align-items: stretch;
  border: var(--hairline-ink);
  border-radius: 999px;
  overflow: hidden;
}
.langpick a {
  display: flex;
  align-items: center;
  padding: 0.3rem 0.7rem;
  font-size: 0.8rem;
  font-weight: 600;
  line-height: 1.2;
  text-decoration: none;
  color: color-mix(in oklab, var(--ink) 65%, transparent);
  white-space: nowrap;
}
.langpick a + a { border-left: var(--hairline-ink); }
.langpick a:hover { background: color-mix(in oklab, var(--ink) 5%, transparent); color: var(--ink); }
/* The active one is filled, not just coloured: aria-current carries the
   meaning for anyone not seeing the fill, and the two together mean it
   is never colour alone. */
.langpick a[aria-current="true"] { background: var(--jj-500); color: #fff; }
.langpick a:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 2px; }

/* Nepali needs a touch more line height than Latin at the same size —
   the matras sit above and below the baseline, so a line box tuned to
   Satoshi clips them. Scoped to the Nepali document only, so the
   English side is untouched. */
:root:lang(ne) body { line-height: 1.75; }
:root:lang(ne) .display { letter-spacing: 0; }

.site-nav { display: flex; gap: clamp(1.25rem, 3vw, 2.5rem); }

/* FOUR LINKS DO NOT FIT A PHONE. Adding "Gold rates" pushed the row
   past the viewport at 390px and 45px of the page scrolled sideways —
   so below 40rem the row scrolls horizontally inside the header instead
   of pushing the page. Every link stays REACHABLE this way; measured at
   320-414, scrolling the strip brings "Gold rates" into view and it
   taps. What it is not is DISCOVERABLE — at 390 the strip is 294px wide
   showing through a 172px window, so the fourth link is simply not
   there until somebody thinks to swipe a navigation bar sideways, which
   is not a gesture most people try.

   THIS IS NOW THE NO-JAVASCRIPT PATH ONLY. With script, the links move
   into a drawer below and nothing is hidden behind a swipe. The strip
   stays because it is the honest fallback: four reachable links beat a
   button that cannot open. */
@media (max-width: 40rem) {
  /* Three children in a 65px bar now: brand, nav, language. The nav
     already scrolls; the picker must not shrink, so it is flex: none
     above and the gap comes down instead. */
  .head-inner { gap: 0.6rem; }
  .langpick a { padding: 0.25rem 0.5rem; font-size: 0.75rem; }
  .site-nav {
    gap: 1rem;
    overflow-x: auto;
    scrollbar-width: none;
    -webkit-overflow-scrolling: touch;
    /* The fade says there is more to the right without drawing a control. */
    mask-image: linear-gradient(to right, #000 0, #000 calc(100% - 1.25rem), transparent 100%);
  }
  .site-nav::-webkit-scrollbar { display: none; }
  .site-nav a { font-size: 0.88rem; }
}
.site-nav a {
  text-decoration: none;
  font-size: 0.95rem;
  white-space: nowrap;
  color: color-mix(in oklab, var(--ink) 65%, transparent);
  position: relative;
  padding-block: 0.35rem;
}
.site-nav a:hover { color: var(--ink); }
.site-nav a[aria-current="page"] { color: var(--ink); font-weight: 600; }
.site-nav a[aria-current="page"]::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 2px;
  background: var(--gold);
}

/* A full-bleed assay rule along the very bottom edge — the page stamped
   where it ends, the device the marketing footer closes on. */
.site-foot {
  margin-top: var(--section-pad);
  padding-block: 1.75rem;
  background: var(--showcase);
  border-top: var(--hairline-ink);
  border-bottom: 2px solid var(--gold);
}
.foot-inner {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  justify-content: space-between;
  gap: 1.5rem 2rem;
}

/* The way back to the marketing site. Given real size because it is the
   only exit from this section, and a reader who has finished an article
   has nowhere else to go. */
.foot-back {
  display: block;
  text-decoration: none;
  color: inherit;
  max-width: 32rem;
}
.foot-back-k {
  display: block;
  font-size: var(--text-eyebrow);
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: color-mix(in oklab, var(--ink) 65%, transparent);
}
.foot-back-t {
  display: block;
  margin-top: 0.15rem;
  font-family: var(--font-display);
  font-size: 1.35rem;
  font-weight: 700;
  line-height: 1.15;
  letter-spacing: -0.015em;
  /* The gold underline the rest of the site uses to mark something
     certified or measured — here it marks the product itself. */
  background-image: linear-gradient(var(--gold), var(--gold));
  background-repeat: no-repeat;
  background-position: 0 100%;
  background-size: 100% 0.06em;
  padding-bottom: 0.08em;
}
.foot-back:hover .foot-back-t { color: var(--jj-500); }
.foot-back:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 4px; border-radius: 4px; }
.foot-back-s {
  display: block;
  margin-top: 0.35rem;
  font-size: 0.85rem;
  line-height: 1.4;
  /* 65% is the floor for text under 24px on this ground. */
  color: color-mix(in oklab, var(--ink) 65%, transparent);
}

.foot-who {
  margin: 0;
  font-size: 0.74rem;
  letter-spacing: 0.05em;
  color: color-mix(in oklab, var(--ink) 65%, transparent);
}

/* ── Shared ────────────────────────────────────────────────────────── */
/* Eyebrow: a short gold rule, then mono caps. Matches Eyebrow.astro. */
.eyebrow {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  margin: 0 0 0.85rem;
  font-family: var(--font-body);
  font-size: var(--text-eyebrow);
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--gold);
}
.eyebrow::before {
  content: "";
  flex: none;
  width: 1.75rem;
  height: 1px;
  background: currentColor;
  opacity: 0.6;
}
.eyebrow a { color: inherit; }

.page-open { padding-block: var(--page-top) 0; }
.page-open h1 { margin: 0; font-size: var(--text-display-l); line-height: 1.04; }

/* Prose caps itself. The shell is edge to edge; readability is protected
   per block, never by narrowing the shell. */
.lede {
  max-width: var(--measure);
  margin: 1.25rem 0 0;
  font-size: var(--text-body-l);
  line-height: 1.6;
  color: color-mix(in oklab, var(--ink) 72%, transparent);
}

.meta {
  display: flex;
  flex-wrap: wrap;
  gap: 0.35rem 1rem;
  margin: 0.9rem 0 0;
  font-size: 0.78rem;
  color: color-mix(in oklab, var(--ink) 65%, transparent);
}
.meta .ad { opacity: 0.8; }
.meta .by::before { content: "— "; }

/* ── Sections ──────────────────────────────────────────────────────── */
.strip { padding-block: var(--section-pad) 0; }
.strip-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 1.5rem;
  margin-bottom: 1.75rem;
  padding-bottom: 0.9rem;
  border-bottom: var(--hairline-ink);
}
.strip-head h2, .strip-h { margin: 0; font-size: var(--text-display-m); line-height: 1.15; }
.strip-h {
  margin-bottom: 1.75rem;
  padding-bottom: 0.9rem;
  border-bottom: var(--hairline-ink);
}
.more {
  font-size: 0.88rem;
  text-decoration: none;
  white-space: nowrap;
  font-family: var(--font-body);
  letter-spacing: 0.04em;
}
.more:hover { text-decoration: underline; }

/* ── Banners ───────────────────────────────────────────────────────── */
.banners { background: var(--showcase); border-bottom: var(--hairline-ink); }
.banners .wrap { padding-block: 1.5rem; }
.banner { display: block; margin-bottom: 0.85rem; }
.banner:last-child { margin-bottom: 0; }
/* A declared ratio holds the space before the image arrives. Banners sit at
   the very top, so a shift here moves the whole page. */
.banner img {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: 1200 / 300;
  object-fit: cover;
  border-radius: var(--radius);
  border: var(--hairline-ink);
  background: color-mix(in oklab, var(--ink) 6%, transparent);
}

/* ── Card grid ─────────────────────────────────────────────────────── */
/* The column COUNT climbs with the viewport rather than the cards
   stretching — the marketing feature grid does the same at 2xl. */
.cardgrid {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: clamp(1.25rem, 2vw, 2rem);
  /* min() OR THE TRACK CANNOT SHRINK. A bare minmax(19rem, 1fr) sets a
     HARD 304px floor, so below about a 344px viewport the track is wider
     than the space it sits in and the page scrolls sideways — measured
     at 320, cards ran to 324 against a 320 viewport. min(19rem, 100%)
     lets the column give way once there is less than 19rem to give. */
  grid-template-columns: repeat(auto-fill, minmax(min(19rem, 100%), 1fr));
  /* Cards size to their own content. Stretching them to a common height
     works when every card has the same parts, but an article without a
     cover image would then have to grow a void the height of a picture to
     match one that has it. A shorter card is the honest shape. */
  align-items: start;
}
@media (min-width: 96rem)  { .cardgrid { grid-template-columns: repeat(4, 1fr); } }
@media (min-width: 128rem) { .cardgrid { grid-template-columns: repeat(5, 1fr); } }

.post {
  background: #fff;
  border: var(--hairline-ink);
  border-radius: var(--radius);
  overflow: hidden;
  display: flex;
  flex-direction: column;
  transition: box-shadow 160ms ease, transform 160ms ease;
}
.post:hover { box-shadow: 0 6px 24px -12px rgb(2 42 42 / 0.28); transform: translateY(-2px); }
.post-img { display: block; }
/* aspect-ratio reserves the box before the bytes arrive. Without it every
   card jumps as its picture loads — the one realistic way to break the
   layout-shift budget. */
.post-img img {
  display: block;
  width: 100%;
  aspect-ratio: 16 / 10;
  object-fit: cover;
  background: color-mix(in oklab, var(--ink) 6%, transparent);
}
.post-body {
  padding: 1.25rem 1.4rem 1.4rem;
  display: flex;
  flex-direction: column;
  flex: 1;
}
.post-body .eyebrow { margin-bottom: 0.6rem; }
.post-title { margin: 0 0 0.5rem; font-size: 1.2rem; line-height: 1.22; }
.post-title a { text-decoration: none; color: var(--ink); }
.post:hover .post-title a { color: var(--jj-500); }
.post-sum {
  margin: 0;
  font-size: 0.95rem;
  color: color-mix(in oklab, var(--ink) 72%, transparent);
  /* CLAMPED, because the summary is written for the article page's
     standfirst and can now run to 800 characters. Unclamped, one long
     standfirst makes its card twice the height of the two beside it and
     the grid stops being a grid. The full text is on the article. */
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 3;
  line-clamp: 3;
  overflow: hidden;
}
.post .meta { margin-top: auto; padding-top: 1rem; }
/* A card with no picture gets a little more room at the top so the eyebrow
   is not jammed against the border. */
.post > .post-body:first-child { padding-top: 1.5rem; }

/* ── Events ────────────────────────────────────────────────────────── */
.eventlist { list-style: none; margin: 0; padding: 0; }
.eventrow {
  display: grid;
  grid-template-columns: 12rem 1fr;
  gap: clamp(1.25rem, 3vw, 2.5rem);
  padding-block: 1.5rem;
  border-bottom: var(--hairline-ink);
}
.eventrow:last-child { border-bottom: 0; }
.eventrow .when {
  font-size: 0.8rem;
  line-height: 1.55;
  color: color-mix(in oklab, var(--ink) 65%, transparent);
}
/* The Nepali date leads and is set in ink; the AD date sits under it,
   quieter. Both always shown — neither reader should have to convert. */
.eventrow .when .bs { display: block; color: var(--ink); }
.eventrow .when .ad { display: block; opacity: 0.85; }
.eventrow-title { margin: 0 0 0.3rem; font-size: var(--text-display-s); line-height: 1.2; }
.eventrow-title a { text-decoration: none; color: var(--ink); }
.eventrow-title a:hover { color: var(--jj-500); }
.where {
  margin: 0 0 0.4rem;
  font-size: 0.88rem;
  color: color-mix(in oklab, var(--ink) 72%, transparent);
}
.eventlist.is-past .eventrow { opacity: 0.8; }

/* ── Article / event page ──────────────────────────────────────────── */
.article { padding-block: var(--page-top) 0; }
.article > * { max-width: 52rem; }
.article h1 { margin: 0; font-size: var(--text-display-l); line-height: 1.06; }
.article .lede { margin-top: 1.5rem; font-size: 1.15rem; }

.factlist { margin: 1.75rem 0 0; display: grid; gap: 0.9rem; font-size: 0.86rem; }
.factlist div { display: grid; grid-template-columns: 6rem 1fr; gap: 1rem; }
.factlist dt {
  color: var(--gold);
  text-transform: uppercase;
  letter-spacing: 0.12em;
  font-size: 0.64rem;
  padding-top: 0.2rem;
}
.factlist dd { margin: 0; }
.factlist .ad { display: block; color: color-mix(in oklab, var(--ink) 65%, transparent); }

.cover { margin: 2.25rem 0 0; max-width: none; }
/* A cover is a lead image, not the article. Left to `height: auto` a
   4:3 upload becomes a full-screen slab the reader has to scroll past
   before reaching a single word. A fixed ratio caps it and crops to the
   middle, which is where a cover's subject nearly always is. */
.cover img {
  display: block;
  width: 100%;
  aspect-ratio: 16 / 9;
  object-fit: cover;
  border-radius: 10px;
  border: var(--hairline-ink);
  background: color-mix(in oklab, var(--ink) 6%, transparent);
}

/* Breathing room between the headline and the byline. */
.article .meta { margin-top: 1.1rem; }

/* Writer-authored HTML. It obeys none of the site's own rules, so the
   styling is applied from outside rather than trusted from within. */
.prose { margin-top: 2.25rem; max-width: 66ch; }
/* A last-resort brake, global. The real fix for the page that would not
   wrap is in PastedText, which strips no-break spaces on save; this stops
   whatever nobody predicted next — a pasted URL, a long product code —
   from putting a horizontal scrollbar on the whole document. */
.prose { overflow-wrap: break-word; }
.prose > :first-child { margin-top: 0; }
.prose p { margin: 0 0 1.2rem; }
.prose h2 {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: 1.5rem;
  line-height: 1.2;
  margin: 2.5rem 0 0.8rem;
  letter-spacing: -0.012em;
}
.prose h3 {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: 1.2rem;
  margin: 2rem 0 0.6rem;
}
.prose h4 { font-weight: 700; font-size: 1.05rem; margin: 1.7rem 0 0.5rem; }
.prose ul, .prose ol { margin: 0 0 1.2rem; padding-left: 1.4rem; }
.prose li { margin-bottom: 0.4rem; }
.prose blockquote {
  margin: 2rem 0;
  padding: 0.25rem 0 0.25rem 1.35rem;
  border-left: 2px solid var(--gold);
  color: color-mix(in oklab, var(--ink) 72%, transparent);
  font-style: italic;
}
.prose img { max-width: 100%; height: auto; border-radius: var(--radius); }
.prose hr { border: 0; border-top: var(--hairline-ink); margin: 2.5rem 0; }

/* ── Gallery ───────────────────────────────────────────────────────── */
/* .gallery is GONE. It was the album page's 4:3 crop grid and nothing
   wears it now — the album page is .jgal, see "The album page" below.
   Removed rather than left in place: a dead class that still looks
   plausible is the one somebody reaches for next, and this one's whole
   behaviour was the bug it caused. */

.back {
  margin: var(--section-pad) 0 0;
  padding-top: 1.25rem;
  border-top: var(--hairline-ink);
  font-size: 0.92rem;
}
.back a { text-decoration: none; }
.back a:hover { text-decoration: underline; }

nav[role="navigation"] { margin: 2.5rem 0 0; font-size: 0.9rem; }

@media (max-width: 46rem) {
  .eventrow { grid-template-columns: 1fr; gap: 0.5rem; }
  .factlist div { grid-template-columns: 1fr; gap: 0.15rem; }
  .strip-head { flex-direction: column; align-items: flex-start; gap: 0.5rem; }
}


/* ============================================================
   Banner — one at a time, rounded, auto-advancing
   ============================================================
   Sits inside the page gutter rather than bleeding full width, because a
   rounded corner only reads as rounded when there is ground either side
   of it. Generous radius on purpose: the ask was "not boxy", and 3px on
   an object this large reads as a square with the corners filed off.
   ============================================================ */

.bnr {
  padding-block: clamp(1.25rem, 2.5vw, 2rem) 0;
  --bnr-radius: clamp(12px, 1.4vw, 22px);
}

.bnr-frame {
  position: relative;
  border-radius: var(--bnr-radius);
  overflow: hidden;
  /* Clip the scrolling track to the rounded box. Without isolation the
     corners of a scrolling child can paint outside the radius. */
  isolation: isolate;
  background: color-mix(in oklab, var(--ink) 6%, transparent);
  box-shadow: 0 1px 2px rgb(2 42 42 / 0.06);
}
.bnr-frame:focus-visible { outline: 2px solid var(--jj-500); outline-offset: 3px; }

.bnr-track {
  display: flex;
  margin: 0;
  padding: 0;
  list-style: none;
  overflow-x: auto;
  overscroll-behavior-x: contain;   /* do not trigger the browser back-swipe */
  scroll-snap-type: x mandatory;
  scrollbar-width: none;
}
.bnr-track::-webkit-scrollbar { display: none; }

@media (prefers-reduced-motion: no-preference) {
  .bnr-track { scroll-behavior: smooth; }
}

.bnr-slide {
  position: relative;   /* the countdown overlay positions against this */
  flex: 0 0 100%;
  scroll-snap-align: center;
  scroll-snap-stop: always;
  /* An aspect ratio, so the box is reserved before any image arrives and
     the page cannot jump.
     2.6:1 — one step shallower than it was. Every step wider trades
     height for crop: a 16:9 upload loses more off the top and bottom to
     fill it. That is the cost of a shorter banner, not a free win. */
  aspect-ratio: 13 / 5;
}
@media (max-width: 48rem) { .bnr-slide { aspect-ratio: 2 / 1; } }
@media (max-width: 30rem) { .bnr-slide { aspect-ratio: 16 / 9; } }

.bnr-media { display: block; width: 100%; height: 100%; }
.bnr-media img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  /* Where a human said the subject is, not where an algorithm guessed. */
  object-position: var(--focal-x, 50%) var(--focal-y, 50%);
}
a.bnr-media:focus-visible { outline: 3px solid var(--jj-500); outline-offset: -3px; }

/* Poster mode: the shape is too far from the frame to crop safely, so the
   whole image sits inside it on a flat matte. A FLAT fill, never a blurred
   copy — blur costs a second composite on the phones least able to pay. */
.bnr-slide.is-poster { background: var(--matte, #022A2A); }
.bnr-slide.is-poster .bnr-media img { object-fit: contain; }

/* ── Dots ──────────────────────────────────────────────────────────── */
.bnr-bar {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.9rem;
  padding-top: 0.85rem;
}

.bnr-dots { display: flex; align-items: center; gap: 0.45rem; }
.bnr-dot {
  position: relative;
  width: 7px;
  height: 7px;
  padding: 0;
  border: 0;
  border-radius: 999px;
  cursor: pointer;
  background: color-mix(in oklab, var(--ink) 22%, transparent);
  transition: background-color 200ms ease, width 260ms cubic-bezier(.4,0,.2,1);
}
/* Paints 7px, answers to a 44px tap through a transparent extender — the
   same trick the marketing nav uses on its links. */
.bnr-dot::after {
  content: "";
  position: absolute;
  left: 50%; top: 50%;
  width: 44px; height: 44px;
  transform: translate(-50%, -50%);
}
.bnr-dot:hover { background: color-mix(in oklab, var(--ink) 45%, transparent); }
.bnr-dot.is-on { background: var(--jj-500); width: 22px; }
.bnr-dot:focus-visible { outline: 2px solid var(--jj-500); outline-offset: 3px; }

.bnr-pause {
  display: grid;
  place-items: center;
  width: 1.5rem;
  height: 1.5rem;
  padding: 0;
  border: 0;
  border-radius: 50%;
  cursor: pointer;
  background: none;
  color: color-mix(in oklab, var(--ink) 45%, transparent);
  transition: color 140ms ease, background-color 140ms ease;
}
.bnr-pause:hover {
  color: var(--ink);
  background: color-mix(in oklab, var(--ink) 8%, transparent);
}
.bnr-pause:focus-visible { outline: 2px solid var(--jj-500); outline-offset: 2px; }
.bnr-pause .is-off { display: none; }

@media (prefers-reduced-motion: reduce) {
  .bnr-track { scroll-behavior: auto; }
  .bnr-dot { transition: none; }
}

/* An author `display` beats the browser's own [hidden] rule whatever the
   specificity, so without this the empty bar shows with JavaScript off. */
.bnr-bar[hidden] { display: none; }

/* ============================================================
   Countdown overlay
   ============================================================
   Sits on top of the banner image, placed on a nine-point grid the
   editor picks. Everything about it is chosen in the admin because we
   never get to see the artwork it will land on — an automatic placement
   would sooner or later sit squarely on somebody's headline.
   ============================================================ */

.cd {
  position: absolute;
  z-index: 2;
  display: flex;
  flex-direction: column;
  gap: 0.35rem;
  max-width: min(88%, 30rem);
  /* Never wider than the frame on a phone, where a four-unit row is
     already most of the width. */
}

/* ── Position: the nine-point grid ─────────────────────────────────── */
.cd { --cd-inset: clamp(0.85rem, 2.2vw, 1.75rem); }
.cd--tl, .cd--tc, .cd--tr { top: var(--cd-inset); }
.cd--ml, .cd--mc, .cd--mr { top: 50%; transform: translateY(-50%); }
.cd--bl, .cd--bc, .cd--br { bottom: var(--cd-inset); }
.cd--tl, .cd--ml, .cd--bl { left: var(--cd-inset); align-items: flex-start; text-align: left; }
.cd--tr, .cd--mr, .cd--br { right: var(--cd-inset); align-items: flex-end; text-align: right; }
.cd--tc, .cd--bc { left: 50%; transform: translateX(-50%); align-items: center; text-align: center; }
.cd--mc {
  left: 50%;
  /* Both axes, so the two transforms have to be written together —
     a second `transform` would replace the first, not add to it. */
  transform: translate(-50%, -50%);
  align-items: center;
  text-align: center;
}

/* ── Shape ─────────────────────────────────────────────────────────── */
.cd--card { padding: 0.9rem 1.1rem; border-radius: 12px; }
.cd--pill { padding: 0.7rem 1.35rem; border-radius: 999px; flex-direction: row;
            align-items: center; gap: 0.9rem; }
.cd--pill .cd-label { margin: 0; }
.cd--plain { padding: 0; }

/* ── Tone ──────────────────────────────────────────────────────────── */
/* A panel, not glass. The artwork underneath is unknown, so contrast has
   to come from an opaque-enough ground rather than from hoping the image
   behind it is dark. */
.cd--dark.cd--card, .cd--dark.cd--pill {
  background: rgb(6 28 28 / 0.72);
  color: #fff;
  box-shadow: 0 8px 28px -12px rgb(0 0 0 / 0.5);
}
.cd--light.cd--card, .cd--light.cd--pill {
  background: rgb(255 255 255 / 0.9);
  color: var(--ink);
  box-shadow: 0 8px 28px -12px rgb(0 0 0 / 0.25);
}
/* With no panel the type carries its own contrast, so it gets a shadow.
   This is the setting to avoid on a busy photograph. */
.cd--dark.cd--plain { color: #fff; text-shadow: 0 1px 12px rgb(0 0 0 / 0.65); }
.cd--light.cd--plain { color: var(--ink); text-shadow: 0 1px 12px rgb(255 255 255 / 0.7); }

/* ── Size ──────────────────────────────────────────────────────────── */
.cd--s { --cd-num: 1.15rem; --cd-lab: 0.58rem; --cd-title: 0.7rem; }
.cd--m { --cd-num: 1.75rem; --cd-lab: 0.62rem; --cd-title: 0.78rem; }
.cd--l { --cd-num: clamp(1.9rem, 4vw, 3rem); --cd-lab: 0.68rem; --cd-title: 0.86rem; }

.cd-label {
  margin: 0;
  font-size: var(--cd-title);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  font-weight: 600;
  opacity: 0.85;
}

.cd-units { display: flex; align-items: baseline; gap: 0.3rem; }
.cd-unit { display: flex; flex-direction: column; align-items: center; line-height: 1; }
.cd-unit b {
  font-size: var(--cd-num);
  font-weight: 700;
  /* Tabular figures, or the whole row jostles sideways every second as
     the digits change width. */
  font-variant-numeric: tabular-nums;
  letter-spacing: -0.01em;
}
.cd-unit i {
  font-style: normal;
  font-size: var(--cd-lab);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  opacity: 0.7;
  margin-top: 0.28em;
}
.cd-sep {
  font-size: var(--cd-num);
  line-height: 1;
  opacity: 0.35;
  transform: translateY(-0.12em);
}

.cd-done { margin: 0; font-size: var(--cd-num); font-weight: 700; line-height: 1.1; }

/* The date in words. Carries the meaning when the script has not run,
   and it is what a screen reader reads instead of ticking numbers. */
.cd-when { margin: 0; font-size: var(--cd-lab); opacity: 0.75; letter-spacing: 0.02em; }
.cd--pill .cd-when { display: none; }   /* no room in a pill */

/* An author `display` beats the browser's own [hidden] rule whatever the
   specificity — without this the finished message and the ticking row
   would both show at once. */
.cd [hidden] { display: none !important; }

@media (max-width: 30rem) {
  /* On a small phone the seconds are the first thing to go: four units
     plus separators do not fit beside a rounded corner. */
  .cd--l { --cd-num: 1.5rem; }
  .cd--pill { flex-direction: column; align-items: flex-start; gap: 0.3rem; }
}

/* ============================================================
   Banner text overlay
   ============================================================
   Words on the artwork, as text rather than baked into the picture.
   Placement reuses the countdown's nine-point grid, so an editor learns
   one control and not two — and the request class refuses to let both
   land on the same cell.
   ============================================================ */

/* NOT decoration. The artwork is unknown when this is written, so
   contrast cannot be assumed: white type on a photograph of a bright
   showroom is unreadable. The gradient fades FROM the edge the text sits
   against, so it darkens the type ground and leaves the rest alone. */
.bnr-scrim {
  position: absolute;
  inset: 0;
  z-index: 1;
  pointer-events: none;   /* never intercepts a click meant for the banner */
  border-radius: inherit;
}

.bnr-scrim--dark  { --scrim-ink: 6 28 28; }
.bnr-scrim--light { --scrim-ink: 255 255 255; }

.bnr-scrim--soft.bnr-scrim--bottom {
  background: linear-gradient(to top, rgb(var(--scrim-ink) / 0.75) 0%,
              rgb(var(--scrim-ink) / 0.35) 38%, rgb(var(--scrim-ink) / 0) 68%);
}
.bnr-scrim--soft.bnr-scrim--top {
  background: linear-gradient(to bottom, rgb(var(--scrim-ink) / 0.75) 0%,
              rgb(var(--scrim-ink) / 0.35) 38%, rgb(var(--scrim-ink) / 0) 68%);
}
.bnr-scrim--soft.bnr-scrim--even { background: rgb(var(--scrim-ink) / 0.42); }

.bnr-scrim--strong.bnr-scrim--bottom {
  background: linear-gradient(to top, rgb(var(--scrim-ink) / 0.92) 0%,
              rgb(var(--scrim-ink) / 0.65) 45%, rgb(var(--scrim-ink) / 0.15) 85%);
}
.bnr-scrim--strong.bnr-scrim--top {
  background: linear-gradient(to bottom, rgb(var(--scrim-ink) / 0.92) 0%,
              rgb(var(--scrim-ink) / 0.65) 45%, rgb(var(--scrim-ink) / 0.15) 85%);
}
.bnr-scrim--strong.bnr-scrim--even { background: rgb(var(--scrim-ink) / 0.68); }

.ov {
  position: absolute;
  z-index: 3;             /* above the scrim, above the countdown panel */
  display: flex;
  flex-direction: column;
  gap: 0.55rem;
  max-width: min(90%, 34rem);
  /* The block must not swallow clicks aimed at a whole-banner link.
     The button inside turns them back on for itself. */
  pointer-events: none;
}
.ov > * { pointer-events: auto; }

.ov { --ov-inset: clamp(1rem, 3vw, 2.5rem); }
.ov--tl, .ov--tc, .ov--tr { top: var(--ov-inset); }
.ov--ml, .ov--mc, .ov--mr { top: 50%; transform: translateY(-50%); }
.ov--bl, .ov--bc, .ov--br { bottom: var(--ov-inset); }
.ov--tl, .ov--ml, .ov--bl { left: var(--ov-inset); align-items: flex-start; text-align: left; }
.ov--tr, .ov--mr, .ov--br { right: var(--ov-inset); align-items: flex-end; text-align: right; }
.ov--tc, .ov--bc { left: 50%; transform: translateX(-50%); align-items: center; text-align: center; }
.ov--mc {
  left: 50%;
  /* Both axes in one declaration — a second `transform` replaces the
     first rather than adding to it. */
  transform: translate(-50%, -50%);
  align-items: center;
  text-align: center;
}

/* Fluid, because this type sits over a picture that is itself scaling.
   A fixed headline that fits at 1440 covers the subject face at 380. */
.ov--s { --ov-h: clamp(1.05rem, 2.4vw, 1.5rem);  --ov-sub: clamp(0.8rem, 1.2vw, 0.9rem); }
.ov--m { --ov-h: clamp(1.35rem, 3.6vw, 2.4rem);  --ov-sub: clamp(0.85rem, 1.5vw, 1.05rem); }
.ov--l { --ov-h: clamp(1.7rem, 5vw, 3.4rem);     --ov-sub: clamp(0.9rem, 1.8vw, 1.2rem); }

/* `dark` means a dark treatment — white type over a dark scrim — matching
   what the same word means on the countdown. */
.ov--dark  { color: #fff; }
.ov--light { color: var(--ink); }

.ov-headline {
  margin: 0;
  font-family: var(--font-display, Georgia, serif);
  font-weight: 700;
  font-size: var(--ov-h);
  line-height: 1.08;
  letter-spacing: -0.015em;
  text-wrap: balance;
}
/* Belt and braces over the scrim: the gradient handles the average case,
   this handles the bright detail that falls behind one letter. */
.ov--dark  .ov-headline { text-shadow: 0 2px 18px rgb(0 0 0 / 0.45); }
.ov--light .ov-headline { text-shadow: 0 2px 18px rgb(255 255 255 / 0.55); }

.ov-subtext {
  margin: 0;
  font-size: var(--ov-sub);
  line-height: 1.45;
  max-width: 44ch;
  opacity: 0.92;
}

.ov-cta {
  display: inline-block;
  margin-top: 0.35rem;
  padding: 0.65rem 1.4rem;
  border-radius: 999px;
  font-size: clamp(0.82rem, 1.1vw, 0.95rem);
  font-weight: 600;
  letter-spacing: 0.01em;
  text-decoration: none;
  transition: transform 140ms ease, box-shadow 140ms ease, background-color 140ms ease;
}
.ov--dark .ov-cta {
  background: #fff;
  color: var(--jj-900);
  box-shadow: 0 10px 30px -14px rgb(0 0 0 / 0.7);
}
.ov--light .ov-cta {
  background: var(--jj-500);
  color: #fff;
  box-shadow: 0 10px 30px -14px rgb(0 0 0 / 0.45);
}
.ov-cta:hover { transform: translateY(-1px); }
.ov--dark .ov-cta:hover  { background: var(--jj-100); }
.ov--light .ov-cta:hover { background: var(--jj-700); }

/* The ring has to read on a white button, on a teal one, and over an
   unknown photograph — so it is drawn in the tone opposite with an
   offset, not in the page usual teal. */
.ov--dark .ov-cta:focus-visible  { outline: 3px solid #fff; outline-offset: 3px; }
.ov--light .ov-cta:focus-visible { outline: 3px solid var(--jj-500); outline-offset: 3px; }

@media (prefers-reduced-motion: reduce) {
  .ov-cta { transition: none; }
  .ov-cta:hover { transform: none; }
}

@media (max-width: 30rem) {
  /* The frame is 16:9 and about 200px tall here. Headline plus subtitle
     plus button does not fit, and the headline is the part that has to
     survive — so the supporting line goes and the button stays. */
  .ov { gap: 0.4rem; max-width: 94%; }
  .ov-subtext { display: none; }
  .ov--l { --ov-h: 1.5rem; }
  .ov-cta { padding: 0.5rem 1.05rem; }
}

/* ============================================================
   Articles — the lead, and the plate
   ============================================================
   The page used to be one flat grid, and it had two faults that
   compounded each other.

   First, no hierarchy: identical cards make every article equally
   important, which means none of them are. On a page that will usually
   hold three or four pieces, one given real size IS most of the design.

   Second, and the reason it looked broken: cards with a cover picture
   were half again as tall as cards without, in a grid that gives every
   cell in a row the same height. One tall card beside two short ones
   reads as a layout that failed to load. Every card now has a head —
   a picture, or a plate — so the rhythm holds whatever is uploaded.
   ============================================================ */

.lead {
  /* The page title sits directly above and .page-open carries no bottom
     margin of its own, so without this the lead image starts on the h1's
     baseline and the two read as one block. */
  margin-top: clamp(1.5rem, 3vw, 2.75rem);
  display: grid;
  grid-template-columns: 1fr;
  gap: clamp(1.25rem, 2.5vw, 2.25rem);
  margin-bottom: clamp(2.5rem, 5vw, 4rem);
}

@media (min-width: 60rem) {
  .lead {
    /* Picture slightly wider than the words. Equal halves read as two
       things side by side; unequal reads as one thing with a picture. */
    grid-template-columns: 1.15fr 1fr;
    align-items: center;
  }
}

.lead-media {
  display: block;
  border-radius: var(--radius);
  overflow: hidden;
  background: color-mix(in oklab, var(--ink) 5%, transparent);
}
.lead-media img,
.lead-media picture {
  display: block;
  width: 100%;
  /* Holds the box before the bytes land. Without it the whole page
     shifts down as the lead image arrives, and this one is above the
     fold on every visit. */
  aspect-ratio: 16 / 10;
  object-fit: cover;
}
.lead-media img { height: 100%; }

.lead-body { min-width: 0; }

.lead-flag {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  margin: 0 0 0.75rem;
}
/* The category sits after the flag, divided by a rule rather than a
   bullet — the same gold hairline the eyebrows use elsewhere. */
.lead-cat {
  display: inline-flex;
  align-items: center;
  gap: 0.6rem;
  color: color-mix(in oklab, var(--ink) 55%, transparent);
}
.lead-cat::before {
  content: "";
  width: 1.5rem;
  height: 1px;
  background: color-mix(in oklab, var(--ink) 25%, transparent);
}

.lead-title {
  margin: 0 0 0.7rem;
  /* Deliberately close to the h1 above it. The page title is a label;
     this is the thing you are meant to read. */
  font-size: clamp(1.6rem, 3.6vw, 2.6rem);
  line-height: 1.08;
  letter-spacing: -0.02em;
  text-wrap: balance;
}
.lead-title a { text-decoration: none; color: var(--ink); }
.lead:hover .lead-title a { color: var(--jj-500); }

.lead-sum {
  margin: 0 0 1.1rem;
  font-size: clamp(1rem, 1.4vw, 1.15rem);
  line-height: 1.55;
  color: color-mix(in oklab, var(--ink) 72%, transparent);
  max-width: 46ch;
  /* Four rather than the cards' three: the lead has a taller picture
     beside it to fill. Same reason — see .post-sum. */
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 4;
  line-clamp: 4;
  overflow: hidden;
}

.lead-more {
  display: inline-block;
  margin-top: 1rem;
  font-weight: 600;
  font-size: 0.95rem;
  color: var(--jj-500);
}
.lead:hover .lead-more { text-decoration: underline; text-underline-offset: 3px; }

/* With no picture the lead does not need to be half the screen wide —
   a plate stretched across 56vw is a lot of nothing. */
@media (min-width: 60rem) {
  .lead.is-plain { grid-template-columns: 0.8fr 1fr; }
}

/* ── The divider before the rest ────────────────────────────────── */
.rule-head {
  display: flex;
  align-items: center;
  gap: 1rem;
  margin: 0 0 clamp(1.25rem, 2vw, 1.75rem);
  font-family: var(--font-body);
  font-size: 0.72rem;
  font-weight: 700;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: color-mix(in oklab, var(--ink) 55%, transparent);
}
.rule-head::after {
  content: "";
  flex: 1;
  height: 1px;
  background: color-mix(in oklab, var(--ink) 14%, transparent);
}

/* ── The plate ──────────────────────────────────────────────────── */
/* THE UNDERLINE HAS TO BE CANCELLED ON THE ANCHOR, NOT ON THE PLATE.
   text-decoration propagates from an ancestor to its in-flow descendants
   and a descendant CANNOT switch it off — `text-decoration: none` on the
   plate itself computes correctly, reports `none`, and the rule is still
   painted under the initial by the link above it. */
.lead-media,
.post-img { text-decoration: none; }

.plate {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 0.5rem;
  width: 100%;
  /* THE SAME BOX A PICTURE WOULD OCCUPY. That is the entire job — the
     row heights stop depending on which articles happen to have art. */
  aspect-ratio: 16 / 10;
  text-align: center;
  padding: 1rem;
}
.plate--big { aspect-ratio: 16 / 10; }

.plate-glyph {
  font-size: clamp(2.5rem, 7vw, 4rem);
  line-height: 1;
  letter-spacing: -0.04em;
  color: color-mix(in oklab, var(--jj-900) 42%, transparent);
}
.plate--big .plate-glyph { font-size: clamp(4rem, 12vw, 8rem); }

.plate-label {
  font-size: 0.68rem;
  font-weight: 600;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: color-mix(in oklab, var(--jj-900) 55%, transparent);
}
.plate--big .plate-label { font-size: 0.78rem; }

/* Four tints, picked by id so a given article always looks the same.
   All are washes of the brand teal and the assay gold — a plate must
   read as part of this site, not as a coloured placeholder. */
.plate--0 { background: color-mix(in oklab, var(--jj-500) 12%, var(--showcase)); }
.plate--1 { background: color-mix(in oklab, var(--assay-deep) 12%, var(--showcase)); }
.plate--2 { background: color-mix(in oklab, var(--jj-300) 16%, var(--showcase)); }
.plate--3 { background: color-mix(in oklab, var(--jj-900) 10%, var(--showcase)); }

/* ── Card corrections that follow from the above ────────────────── */
/* Every card now has a head, so the "no picture gets extra padding"
   rule has nothing left to correct and would just add a dead band. */
.post > .post-body:first-child { padding-top: 1.25rem; }

.post-img picture { display: block; }
.post-img img {
  display: block;
  width: 100%;
  aspect-ratio: 16 / 10;
  object-fit: cover;
}

/* Cards stretch to the tallest in their row and their meta line is
   pushed to the bottom by .post .meta { margin-top: auto }, so the
   dates line up across a row instead of floating wherever the summary
   happens to end. */
.cardgrid { align-items: stretch; }
.post { height: 100%; }

/* ============================================================
   The article page
   ============================================================
   ONE COLUMN, CENTRED, AND ONLY THE COVER BREAKS OUT.

   The measure is set in rem, NOT in ch. `ch` is the width of the digit
   zero, which in Satoshi is far wider than an average lowercase letter —
   the old `max-width: 66ch` measured 766px and carried 84 characters per
   line, well past the 50-75 that reading research settles on. Naming a
   width in ch reads as if it means characters and does not.

   THE 66 CHARACTERS THIS FILE USED TO CLAIM FOR 40rem AT 18px WAS
   WRONG. Measured properly — walking a Range along a paragraph's own
   text node and reading the offsets where the line box changes, which is
   the only method that sees what actually rendered — that pair was 78
   characters a line, already past the band it was written to satisfy.
   Dividing a paragraph's length by its line count, the obvious way,
   understates it: the last line of every paragraph is a partial one.

   THE TEXT NOW MATCHES THE COVER AT 896px, AND THAT IS A TRADE MADE ON
   PURPOSE. Alignment won over the measure. A 44rem column under a 56rem
   picture stepped in by 96px at every paragraph, and that ragged left
   edge is the first thing you see on the page — worse, in practice, than
   a line that runs long.

   THE TYPE AND THE LEADING PAY FOR IT. Measured at 896px:
       21px  97 cpl      24px  84 cpl
       22px  92          25px  78
       23px  88          26px  76
   24px at 1.75 line-height is where it sits: 84 characters, above the
   50-75 the research points at, and the extra leading is the standard
   mitigation — a long line needs more space beneath it for the eye to
   find the start of the next one. 26px would have bought 76 characters
   and read as a large-print edition.

   IF THE LINE EVER NEEDS TO COME BACK INTO THE BAND, narrow the COVER to
   meet the text rather than un-aligning them again: --art-wide down to
   44rem puts both at 704px and 71 characters.

   Re-measure with the Range walk if either number changes.
   ============================================================ */

/* SCOPED TO .longform, NOT .article. `.article` is also worn by the event
   and album pages — capping it at 56rem would have squeezed the album
   gallery, which is deliberately wide. The article page gets its own
   class so this redesign cannot reach pages it was not designed for. */
.longform {
  /* Centres the whole column, gutters included, without needing .wrap. */
  width: 100%;
  /* THE GUTTER IS ADDED, NOT ABSORBED. max-width: var(--art-wide) caps
     the PADDED BOX, so the text inside it was --art-wide minus two
     gutters — 794px at 1280 and 814px at 1024, never the 896 it claims.
     Above 1440 the grid gives the column exactly 896, so the same
     article was 794 wide at one width and 896 at another for no reason
     the reader could see. Adding the gutters makes the CONTENT the
     token, which is what the number is supposed to mean. */
  max-width: calc(var(--art-wide) + 2 * var(--gutter));
  margin-inline: auto;
  padding-inline: var(--gutter);
  padding-block: var(--page-top) 0;
}

/* Everything is the reading column; the cover overrides this on its own. */
.longform > * {
  max-width: var(--art-measure);
  margin-inline: auto;
}

/* ── Header ─────────────────────────────────────────────────────── */
.longform .art-head { margin-bottom: 2rem; }

.longform .art-head .eyebrow {
  display: flex;
  align-items: center;
  gap: 0.55rem;
  margin: 0 0 1rem;
}
.longform .art-head .eyebrow a { color: inherit; }

/* A hairline instead of a middle dot. A dot between two pieces of
   letter-spaced uppercase reads as a full stop at this size. */
.longform .sep {
  display: inline-block;
  width: 1.1rem;
  height: 1px;
  background: currentColor;
  opacity: 0.4;
}

.longform .art-title {
  margin: 0;
  /* Tighter than the list pages: a headline this size needs its lines
     closer together or the two halves stop reading as one sentence.

     RAISED WITH THE BODY, because it did not move when the body went
     from 18px to 24px and the ladder quietly compressed: the headline
     was 2.76x the body and became 2.07x, which is what "the title and
     the text look the same size" actually was. 4.25rem puts it back to
     2.83x. The vw term is 7 rather than 5 so the ratio holds down the
     middle widths too — the body is a fixed 24px from 640px up, so a
     headline that scales with the viewport diverges from it. */
  font-size: clamp(2.5rem, 7vw, 4.25rem);
  line-height: 1.04;
  letter-spacing: -0.025em;
  text-wrap: balance;
}

.longform .art-lede {
  margin: 1.15rem 0 0;
  /* Raised with the body. At the old ceiling of 1.3rem it was 20.8px
     against a 21px body — a standfirst the same size as the article
     stops reading as a standfirst. The FLOOR is what matters, not the
     ceiling: 2.2vw collapses to it from about 1360px down, so the floor
     is what runs at most desktop widths and it has to clear the 24px
     body rather than approach it. An earlier 1.25rem floor put the
     standfirst at 20px under a 21px body between 640 and 1024. */
  font-size: clamp(1.625rem, 2.2vw, 1.875rem);
  line-height: 1.5;
  color: color-mix(in oklab, var(--ink) 70%, transparent);
  text-wrap: pretty;
}

.longform .byline {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.4rem 0.9rem;
  margin-top: 1.5rem;
  padding-top: 1.1rem;
  border-top: 1px solid color-mix(in oklab, var(--ink) 12%, transparent);
  font-size: 0.88rem;
}
.longform .by-who { font-weight: 600; }
.longform .by-meta {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem;
  color: color-mix(in oklab, var(--ink) 62%, transparent);
}

/* ── Cover ──────────────────────────────────────────────────────── */
/* The ONE element allowed past the measure. Wider than the text is what
   makes it read as the article's opening image rather than as a picture
   dropped into the middle of a paragraph. */
.longform .art-cover {
  max-width: var(--art-wide);
  margin: 0 auto 2.5rem;
}
.longform .art-cover picture { display: block; }
.longform .art-cover img {
  display: block;
  width: 100%;
  /* 16:9, cropped. Left at auto a 4:3 upload becomes a slab the reader
     scrolls past before reaching a single word. */
  aspect-ratio: 16 / 9;
  object-fit: cover;
  border-radius: 12px;
  background: color-mix(in oklab, var(--ink) 5%, transparent);
}
.longform .art-cover figcaption {
  margin-top: 0.7rem;
  font-size: 0.82rem;
  line-height: 1.5;
  color: color-mix(in oklab, var(--ink) 58%, transparent);
}

/* ── The prose ──────────────────────────────────────────────────── */
/* Writer-authored HTML. It obeys none of the site's own rules, so the
   styling is applied from outside rather than trusted from within. */
.longform .prose {
  max-width: var(--art-measure);
  margin-top: 0;
  /* 24px, and 1.75 leading rather than 1.7. Both are doing the same job:
     holding an 896px line together. See the table above. */
  font-size: 1.5rem;
  line-height: 1.75;
  color: color-mix(in oklab, var(--ink) 88%, transparent);
}

/* A LAST-RESORT BRAKE. The real fix for the page that would not wrap is
   in PastedText, which strips the no-break spaces on save. This is the
   safety net for the next thing nobody predicted — a pasted URL, a long
   product code — so that whatever it is cannot put a horizontal
   scrollbar on the whole document again. */
.longform .prose { overflow-wrap: break-word; }

.longform .prose > :first-child { margin-top: 0; }
.longform .prose p { margin: 0 0 1.35em; }
.longform .prose > :last-child { margin-bottom: 0; }

.longform .prose h2,
.longform .prose h3,
.longform .prose h4 {
  font-family: var(--font-display);
  font-weight: 700;
  letter-spacing: -0.015em;
  line-height: 1.2;
  /* Far more space above than below, so a heading belongs to the text
     that FOLLOWS it rather than floating between two blocks. */
  margin: 2.5em 0 0.6em;
}
/* Opened up with the rest of the ladder. At 1.5/1.24/1.08 of a 24px
   body these were 36/30/26 — an h2 and an h3 six pixels apart, and an h4
   two pixels off the paragraph it introduces. */
.longform .prose h2 { font-size: 1.6em; }
.longform .prose h3 { font-size: 1.3em; }
.longform .prose h4 { font-size: 1.1em; }

.longform .prose ul,
.longform .prose ol { margin: 0 0 1.35em; padding-left: 1.4em; }
.longform .prose li { margin-bottom: 0.5em; }
.longform .prose li::marker { color: var(--gold); }

.longform .prose a {
  color: var(--jj-500);
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 2px;
}
.longform .prose a:hover { text-decoration-thickness: 2px; }

.longform .prose strong { font-weight: 700; color: var(--ink); }

/* ── Alignment from the editor ──────────────────────────────────────
   Quill 2 writes alignment as these classes. Only these three names get
   through the allowlist in config/purifier.php, and left needs no class
   because left is the absence of one. */
.longform .prose .ql-align-center  { text-align: center; }
.longform .prose .ql-align-right   { text-align: right; }
.longform .prose .ql-align-justify {
  text-align: justify;
  /* HYPHENATION IS PART OF JUSTIFYING, not a nicety. Justified text
     works by stretching the spaces, and on an 896px line with no break
     points the stretch collects into white rivers running down the
     paragraph. The page is lang="en", so the browser can break words. */
  hyphens: auto;
  /* Never stretch a space past this to avoid one. */
  text-align-last: start;
}

.longform .prose blockquote {
  margin: 2em 0;
  padding-left: 1.25em;
  border-left: 2px solid var(--gold);
  font-size: 1.08em;
  line-height: 1.55;
  color: color-mix(in oklab, var(--ink) 74%, transparent);
}
.longform .prose blockquote p:last-child { margin-bottom: 0; }

.longform .prose img {
  max-width: 100%;
  height: auto;
  border-radius: 10px;
  margin: 1.75em 0;
}

.longform .prose hr {
  border: 0;
  height: 1px;
  background: color-mix(in oklab, var(--ink) 14%, transparent);
  margin: 2.75em 0;
}

/* ── Tags, back link ────────────────────────────────────────────── */
.longform .art-tags {
  list-style: none;
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  margin: 2.5rem auto 0;
  padding: 0;
}
.longform .art-tags li {
  padding: 0.3rem 0.75rem;
  border: 1px solid color-mix(in oklab, var(--ink) 15%, transparent);
  border-radius: 999px;
  font-size: 0.8rem;
  color: color-mix(in oklab, var(--ink) 70%, transparent);
}

.longform .art-back {
  margin: 2.5rem auto 0;
  padding-top: 1.5rem;
  border-top: 1px solid color-mix(in oklab, var(--ink) 12%, transparent);
  font-size: 0.92rem;
}
.longform .art-back a { font-weight: 600; text-decoration: none; }
.longform .art-back a:hover { text-decoration: underline; text-underline-offset: 3px; }

/* ── Read next ──────────────────────────────────────────────────── */
/* On its own ground, so the end of the article is unmistakable. Without
   the tint the related cards read as more of the same piece. */
.art-more {
  margin-top: clamp(3rem, 6vw, 5rem);
  padding-block: clamp(2.5rem, 5vw, 4rem);
  background: color-mix(in oklab, var(--ink) 4%, transparent);
  border-top: 1px solid color-mix(in oklab, var(--ink) 10%, transparent);
}

@media (max-width: 40rem) {
  /* 18px on a phone. A 350px column takes about 42 characters a line
     whatever the size, so the type is free here — the column width does
     the limiting, not the type, and the desktop pair exists only to
     hold an 896px line together. */
  .longform .prose { font-size: 1.125rem; line-height: 1.65; }
  /* The standfirst comes back down with it. 26px over 18px is a ratio
     that belongs to a desktop measure, not a phone. */
  .longform .art-lede { font-size: 1.375rem; }
  .longform .art-title { line-height: 1.08; }
}

/* ============================================================
   Reading progress
   ============================================================
   A gold rule that fills as you read. Gold because on this site gold is
   the assay mark — it appears where something is measured, dated or
   counted, and a progress bar is a measurement.

   It sits ON the sticky header's bottom edge rather than floating over
   the page. The header is position: sticky, top: 0 and never leaves, so
   a fixed bar at (header height - its own height) lands exactly on that
   border at every scroll position, and the border reads as its track.
   No second track element, no line hovering over the prose.
   ============================================================ */
.readbar {
  position: fixed;
  left: 0;
  top: calc(4.0625rem - 3px);   /* the header is 65px; see .head-inner */
  width: 100%;
  height: 3px;
  z-index: 51;                  /* the header is 50 */
  pointer-events: none;
  transition: opacity 260ms ease;
}
/* Set by the script the moment the last line of the article has been
   reached. See readbar.js. */
.readbar.is-done { opacity: 0; }

.readbar i {
  display: block;
  height: 100%;
  background: var(--gold);
  /* SCALED, NOT WIDTHED. Width is layout; a transform is composited, so
     the bar costs nothing on a scroll that is already doing work. */
  transform: scaleX(0);
  transform-origin: 0 50%;
  will-change: transform;
}

@media print { .readbar { display: none; } }

/* ============================================================
   The margin card
   ============================================================
   THE ARTICLE DOES NOT MOVE.

   The obvious way to add a sidebar is to lay the page out as
   [article][rail] and centre the pair, which shifts the reading column
   152px off centre at every width. This page was right before the card
   existed, so instead .longform becomes a three-column grid:

       1fr | 56rem | 1fr

   The middle column is the same shell at the same width with equal
   space either side, so the text lands on exactly the pixel it landed
   on before. The card goes in column 3, which was empty anyway.

   Below the threshold the grid is not declared at all and the card is
   an ordinary block in the reading column at the foot of the article —
   same element, no duplicate markup.
   ============================================================ */

/* In the flow, below the threshold. */
.art-aside { margin-top: 2.5rem; }

.art-aside-h {
  margin: 0 0 0.75rem;
  font-family: var(--font-body);
  /* The site's own eyebrow size, 12px. NOT the 0.72rem this started as:
     that is 11.5px, under the 12px floor, and at 58% ink it measured
     below AA as well. --text-eyebrow at 65% clears both. (.engage-h is
     still 11.84px at 58% — pre-existing, listed in CONTENT-TODO.) */
  font-size: var(--text-eyebrow);
  font-weight: 700;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: color-mix(in oklab, var(--ink) 65%, transparent);
}

.nextev {
  display: block;
  padding: 1rem 1.1rem;
  border: 1px solid color-mix(in oklab, var(--ink) 14%, transparent);
  border-radius: 10px;
  background: #fff;
  text-decoration: none;
  color: var(--ink);
  transition: border-color 140ms ease, transform 140ms ease;
}
.nextev:hover {
  border-color: color-mix(in oklab, var(--jj-500) 45%, transparent);
  transform: translateY(-1px);
}
.nextev:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 2px; }

.nextev-when {
  display: block;
  font-size: 0.78rem;
  line-height: 1.5;
  color: color-mix(in oklab, var(--ink) 65%, transparent);
}
.nextev-when .bs { display: block; color: var(--ink); }
.nextev-when .ad { display: block; opacity: 0.85; }

.nextev-title {
  display: block;
  margin-top: 0.5rem;
  font-size: 1.15rem;
  line-height: 1.25;
  letter-spacing: -0.01em;
}
.nextev:hover .nextev-title { color: var(--jj-500); }

.nextev-where {
  display: block;
  margin-top: 0.35rem;
  font-size: 0.85rem;
  line-height: 1.45;
  /* 65% is the floor for text under 24px on this ground — see the
     contrast table in CLAUDE.md. Do not take it lighter. */
  color: color-mix(in oklab, var(--ink) 72%, transparent);
}

.nextev-in {
  display: inline-block;
  margin-top: 0.7rem;
  padding: 0.15rem 0.5rem;
  border-radius: 999px;
  background: color-mix(in oklab, var(--gold) 16%, transparent);
  font-size: 0.75rem;
  color: color-mix(in oklab, var(--ink) 78%, transparent);
}

.art-aside-more {
  display: inline-block;
  margin-top: 0.8rem;
  font-size: 0.85rem;
  font-weight: 600;
  color: var(--jj-500);
  text-decoration: none;
}
.art-aside-more:hover { text-decoration: underline; text-underline-offset: 3px; }

/* ── In the margin ──────────────────────────────────────────────────
   90rem = 1440px, and it is derived, not picked. With the gutter at 4vw
   and the gap at 2.5vw, the third column works out at (0.87V - 896) / 2
   — so 1440 is the first width where it reaches 178px, which is the
   narrowest this card was still legible at when measured. Below it the
   flow fallback is better than a squeezed rail.

   RE-DERIVE THIS IF --art-wide CHANGES. Widening the shell narrows the
   margin twice as fast as it widens the page. */
@media (min-width: 90rem) {
  .longform {
    max-width: none;
    display: grid;
    /* The middle column is the shell. The two 1fr columns are equal, so
       it stays centred to the pixel. */
    grid-template-columns: 1fr var(--art-wide) 1fr;
    column-gap: clamp(1.5rem, 2.5vw, 3rem);
    align-items: start;
  }

  /* width: 100% IS LOAD-BEARING HERE. A grid item with margin-inline:
     auto — which every child carries, to centre it in the block layout —
     is shrink-to-fit, not stretched. So the tag list measured 139px and
     "All articles" 89px, both CENTRED under an 896px column while every
     paragraph above them was left-aligned. It reads as the foot of the
     article being a different element from the article. */
  .longform > * { grid-column: 2; width: 100%; }

  .longform > .art-aside {
    grid-column: 3;
    /* ROW 2, so the card starts below the headline block rather than
       beside it — .art-head is always the first child, so row 2 is
       "after the title" whether or not there is a cover.

       The end is `span 50` and not `-1` on purpose: with no
       grid-template-rows declared, every row here is implicit and a
       negative line number resolves against the EXPLICIT grid, which
       has one line — so `2 / -1` silently collapses to row 1. 50 is
       simply more rows than an article can have. */
    grid-row: 2 / span 50;
    margin-top: 0;
    /* The sticky child needs a tall containing block to travel inside,
       which is what the row span above provides. */
    align-self: stretch;
  }

  .art-aside-in {
    position: sticky;
    /* Clears the 65px header and then breathes. */
    top: 6rem;
    max-width: 17rem;
  }
}

/* Sticky is a scroll effect; without motion it is just a card that
   stays put, which is fine — but a reader who asked for no motion gets
   the quieter version. */
@media (prefers-reduced-motion: reduce) {
  .nextev { transition: none; }
  .nextev:hover { transform: none; }
  .readbar { transition: none; }
}


/* ============================================================
   The home page
   ============================================================
   A FRONT PAGE, NOT THREE OF THE SAME MODULE.

   It was a heading-with-a-link followed by a row of three equal cards,
   three times over. That construction has no lead: every article, event
   and album carried the same weight, so nothing on the page told a
   first-time visitor where to begin, and the three blocks were
   indistinguishable from each other at a glance.

   The shape now is the one every editorial home page converges on:

     masthead      a band, not a fold
     front page    ONE lead at real size + the rest as headlines beside it
     diary         dark, date-led — because events are not articles
     gallery       a mosaic, image first — because albums are not articles

   Each module is shaped differently ON PURPOSE. Rhythm is what makes a
   long page navigable; three identical blocks is a list.
   ============================================================ */

/* ── Masthead ───────────────────────────────────────────────────────
   The old opening was .page-open: a var(--text-display-l) title over a
   one-line description, which spent most of the first fold saying the
   name of a page the reader had already chosen to visit. */
.masthead {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  justify-content: space-between;
  gap: 1rem 2rem;
  padding-block: var(--page-top) 1.1rem;
  margin-bottom: clamp(1.75rem, 3vw, 2.75rem);
  border-bottom: 2px solid color-mix(in oklab, var(--ink) 82%, transparent);
}
.masthead-id { min-width: 0; }
.masthead-title {
  margin: 0;
  /* Smaller than a page title elsewhere on this site. This is a
     nameplate — the lead's headline below it is the thing to read, and
     a masthead that outshouts the news is a poster. Measured at the
     first attempt: a 48px nameplate against a 54.4px headline, a ratio
     of 1.13, and on a phone the two landed on 32px exactly. This puts
     the headline 1.5x above it on desktop and 1.33x on a phone. */
  font-size: clamp(1.5rem, 3vw, 2.25rem);
  line-height: 1;
}
.masthead-meta {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 0.1rem;
  margin: 0;
  font-size: 0.82rem;
  line-height: 1.4;
  text-align: right;
  color: color-mix(in oklab, var(--ink) 65%, transparent);
}
.masthead-meta .bs { color: var(--ink); font-weight: 600; }

@media (max-width: 34rem) {
  .masthead { align-items: flex-start; }
  .masthead-meta { align-items: flex-start; text-align: left; }
}

/* ── Front page ─────────────────────────────────────────────────────
   Lead left, headlines right. The proportions are the whole argument:
   at anything near equal the two columns read as two lists and the page
   loses the lead it exists to have. */
.frontpage {
  display: grid;
  gap: clamp(2rem, 4vw, 3.5rem);
  /* BARELY ANY BOTTOM PADDING — A COLOUR CHANGE IS ALREADY THE
     SEPARATION. This carried 80px, and what follows it is the dark diary
     band. So the page put a light strip between a light sticky header
     and a hard dark edge, which reads as a stray band rather than as
     breathing room. The dark section spends 68px of its own INSIDE the
     colour, which is where the separation belongs.

     BUT NOT ZERO, WHICH WAS THE FIRST FIX AND OVERSHOT. At 1440 the last
     thing in this grid is the lead article's meta line, and with no
     padding its line box ended exactly on the colour boundary —
     measured 0px — so the type read as guillotined by the band rather
     than as ending. 1.5rem is a little over one line of that 13px meta
     text: enough that it clears, far too little to read as a strip.
     Measured after, with the reveals settled, the clearance is 24px at
     1024-1920 (where the lead article's meta line is the tail) and 26px
     at 390-768 (where the single column ends on "All articles"), so the
     seam reads the same at every width.

     Safe when the diary is absent: what follows is then .photostrip,
     which has its own top padding, or the footer, which has a margin. */
  padding-bottom: 1.5rem;
}

@media (min-width: 64rem) {
  .frontpage {
    grid-template-columns: minmax(0, 1.85fr) minmax(17rem, 1fr);
    align-items: start;
  }
}

.fp-lead { min-width: 0; }

.fp-lead-media {
  display: block;
  /* It sits under the standfirst now, not above the headline. */
  margin: 0.35rem 0 1.15rem;
  border-radius: var(--radius);
  overflow: hidden;
  background: color-mix(in oklab, var(--ink) 5%, transparent);
}
.fp-lead-media img,
.fp-lead-media picture,
.fp-lead-media .plate {
  display: block;
  width: 100%;
  /* Holds the box before the bytes land. This is the biggest image on
     the site and it is above the fold on every visit, so without a
     declared ratio the whole page steps down as it arrives. 16:9 rather
     than the list page's 16:10 — it is the only picture at this size and
     a shallower crop keeps the fold usable. */
  aspect-ratio: 16 / 9;
  object-fit: cover;
}
.fp-lead-media img { height: 100%; }

.fp-flag { margin-bottom: 0.9rem; }

.fp-lead-title {
  margin: 0 0 0.85rem;
  /* The largest type on the page, and larger than the masthead. On a
     front page the story outranks the nameplate. */
  font-size: clamp(2rem, 4.4vw, 3.4rem);
  line-height: 1.03;
  letter-spacing: -0.025em;
}
.fp-lead-title a { text-decoration: none; color: var(--ink); }
.fp-lead:hover .fp-lead-title a { color: var(--jj-500); }

.fp-lead-sum {
  margin: 0 0 1.1rem;
  max-width: 54ch;
  font-size: clamp(1.05rem, 1.3vw, 1.2rem);
  line-height: 1.55;
  color: color-mix(in oklab, var(--ink) 72%, transparent);
  /* The summary can run to 800 characters — it is written to be the
     article's standfirst, not a card blurb. Three lines here, and the
     whole of it one click away. */
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 3;
  line-clamp: 3;
  overflow: hidden;
}

/* ── The headline column ────────────────────────────────────────────
   A rule down its left edge on desktop, so it reads as a column OF the
   front page rather than as a block that happens to sit beside it. */
.fp-more { min-width: 0; }

@media (min-width: 64rem) {
  .fp-more {
    padding-left: clamp(1.5rem, 2.5vw, 2.5rem);
    border-left: var(--hairline-ink);
  }
}

.fp-more-h {
  /* 0.9rem, not the 0.4 it carried. The list's own rhythm is 31px from
     one item's date to the next item's title (16 above the rule, 15
     below); at 0.4rem this label sat 22px above the first title, so the
     heading was tighter to its list than the list was to itself.
     0.9rem + the item's 15.2px padding lands on the same 30px. */
  margin: 0 0 0.9rem;
  font-family: var(--font-body);
  font-size: var(--text-eyebrow);
  font-weight: 700;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: color-mix(in oklab, var(--ink) 65%, transparent);
}

.fp-list {
  list-style: none;
  margin: 0;
  padding: 0;
}

.fp-item { border-bottom: var(--hairline-ink); }
.fp-item:last-child { border-bottom: 0; }

.fp-item-link {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: 0.9rem;
  align-items: start;
  padding: 0.95rem 0;
  text-decoration: none;
  color: inherit;
}
.fp-item-link:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 3px; }

.fp-item-n {
  /* Gold doing its one job on this site: marking something counted. */
  font-size: 0.78rem;
  font-weight: 700;
  letter-spacing: 0.06em;
  color: var(--gold);
  padding-top: 0.15rem;
}

.fp-item-body { min-width: 0; display: block; }

.fp-item-cat {
  display: block;
  margin-bottom: 0.2rem;
  /* 12px, the floor. 0.68rem is 10.88 and this site does not go under
     twelve — the eyebrow token exists so nobody has to remember that. */
  font-size: var(--text-eyebrow);
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: color-mix(in oklab, var(--ink) 65%, transparent);
}

.fp-item-title {
  display: block;
  font-size: 1.08rem;
  font-weight: 700;
  line-height: 1.25;
  letter-spacing: -0.012em;
  text-wrap: pretty;
}
.fp-item-link:hover .fp-item-title { color: var(--jj-500); }

.fp-item-date {
  display: block;
  margin-top: 0.3rem;
  font-size: 0.76rem;
  color: color-mix(in oklab, var(--ink) 65%, transparent);
}

.fp-more-all { display: inline-block; margin-top: 1.1rem; font-weight: 600; }

/* ── Diary ──────────────────────────────────────────────────────────
   The one dark band on the page. See the note in home.blade.php: run
   light, this read as three more articles with dates attached. */
.diary {
  background: var(--jj-900);
  padding-block: clamp(2.75rem, 5vw, 4.25rem);
}

.diary-head {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  justify-content: space-between;
  gap: 1rem 2rem;
  margin-bottom: 1.5rem;
  padding-bottom: 1rem;
  border-bottom: 1px solid rgb(255 255 255 / 0.16);
}
.diary-h { margin: 0; font-size: var(--text-display-m); line-height: 1.15; color: #fff; }
.diary-all { color: var(--jj-100); }

.diary-list { list-style: none; margin: 0; padding: 0; }

.diary-row {
  display: grid;
  grid-template-columns: auto 1fr auto;
  align-items: center;
  gap: 1.25rem;
  padding: 1.15rem 0;
  border-bottom: 1px solid rgb(255 255 255 / 0.10);
}
.diary-row:last-child { border-bottom: 0; }

/* The date block. Bordered rather than filled: a filled tile at this
   size becomes the loudest thing in the band and outweighs the event it
   is there to label. */
.diary-when {
  flex: none;
  display: grid;
  place-items: center;
  width: 4.25rem;
  padding: 0.5rem 0;
  border: 1px solid rgb(255 255 255 / 0.22);
  border-radius: var(--radius);
  line-height: 1;
}
.diary-day { font-size: 1.6rem; font-weight: 700; color: #fff; }
.diary-mon {
  margin-top: 0.2rem;
  /* 12px floor, as .fp-item-cat above. */
  font-size: var(--text-eyebrow);
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--gold);
}

.diary-body { min-width: 0; }

/* SCOPED, because `.on-dark a` is (0,1,1) and a bare .diary-title is
   (0,1,0) — the link colour won and the headline rendered teal. */
.diary .diary-title {
  display: block;
  font-size: clamp(1.15rem, 1.8vw, 1.5rem);
  font-weight: 700;
  line-height: 1.2;
  color: #fff;
  text-decoration: none;
}
.diary .diary-title:hover { color: var(--jj-100); text-decoration: underline; text-underline-offset: 3px; }
.diary .diary-title:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 3px; }

.diary-meta {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.3rem 0.85rem;
  margin-top: 0.35rem;
  font-size: 0.82rem;
  /* NOT color-mix(in oklab, ..., transparent). That is how every muted
     value on this site is written and it is wrong on a dark ground:
     mixing toward `transparent` in oklab interpolates the COLOUR as
     well as the alpha, so the result is pulled toward black. On a light
     ground that raises contrast, which is why it has never bitten
     anyone here — on jj-900 it measured 1.32:1, all but invisible.
     A plain alpha composites without moving the hue: 9.6:1. */
  color: rgb(215 226 226 / 0.82);
}
.diary-where::before {
  content: "";
  display: inline-block;
  width: 1rem;
  height: 1px;
  margin-right: 0.6rem;
  vertical-align: middle;
  background: currentColor;
  opacity: 0.5;
}

.diary-in {
  flex: none;
  padding: 0.25rem 0.7rem;
  border-radius: 999px;
  background: rgb(255 255 255 / 0.10);
  font-size: 0.75rem;
  white-space: nowrap;
  color: var(--jj-100);
}

@media (max-width: 44rem) {
  /* The relative time drops to its own line rather than squeezing the
     title down to two words a line. */
  .diary-row { grid-template-columns: auto 1fr; }
  .diary-in { grid-column: 2; justify-self: start; margin-top: 0.15rem; }
}

/* ── Photographs, on the home page ───────────────────────────────────
   THIS RULE WAS DELETED BY ACCIDENT and shipped that way. It sat inside
   the .mosaic/.tile-* block removed when the section stopped showing
   album covers and started showing photographs, so from that commit the
   section had NO padding at all: it ran flush against the dark diary
   above it and the footer below. Caught by the gap audit, not by eye —
   a section with no padding under a full-bleed dark band looks
   deliberate until you measure it. */
.photostrip { padding-block: clamp(3rem, 6vw, 5rem); }

/* The home page's album mosaic is GONE. It showed four album COVERS —
   four cropped thumbnails standing in for 36 photographs — which is a
   menu of galleries, not a gallery. The section runs the pictures
   themselves now, through .jgal below. Deleted rather than left: a dead
   rule that still looks plausible is the one somebody reaches for. */

}

/* ============================================================
   The album page
   ============================================================
   NO CROPPING ANYWHERE ON THIS PAGE.

   It used to lay every photograph into a 4:3 box with object-fit:
   cover, which is the only layout available when you do not know what
   shape the pictures are. One album holds a 1920x1080, a 2000x1500, a
   1504x2000 and two 923x2000 phone screenshots; a 4:3 crop of a 0.46:1
   image is a strip out of its middle, and one of them rendered as an
   empty grey box because the middle of that screenshot is empty grey.

   Photos carry their real width and height now, so the gallery is
   justified instead — the layout every serious photo site uses for a
   mixed set.
   ============================================================ */

.alb-head {
  padding-block: var(--page-top) 0;
  margin-bottom: clamp(1.75rem, 3vw, 2.5rem);
}

.alb-title {
  margin: 0;
  font-size: clamp(2rem, 4.4vw, 3.25rem);
  line-height: 1.05;
  letter-spacing: -0.022em;
}

.alb-lede {
  max-width: 58ch;
  margin: 1rem 0 0;
  font-size: clamp(1.05rem, 1.3vw, 1.2rem);
  line-height: 1.55;
  color: color-mix(in oklab, var(--ink) 72%, transparent);
}

/* One line for count, provenance and date. They were split either side
   of the description before, which cut the opening in half. */
.alb-meta {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.35rem 1.1rem;
  margin: 1.25rem 0 0;
  padding-top: 1rem;
  border-top: var(--hairline-ink);
  font-size: 0.86rem;
  color: color-mix(in oklab, var(--ink) 65%, transparent);
}
.alb-count { font-weight: 600; color: var(--ink); }
.alb-from { font-weight: 600; }
/* A hairline divider rather than a bullet, as everywhere else here. */
.alb-date::before {
  content: "";
  display: inline-block;
  width: 1.1rem;
  height: 1px;
  margin-right: 1.1rem;
  vertical-align: middle;
  background: currentColor;
  opacity: 0.45;
}

/* ── The justified gallery ──────────────────────────────────────────
   THE WHOLE LAYOUT IS TWO LINES, AND THEY ARE WORTH UNDERSTANDING.

   Each item's flex-basis is its own aspect ratio times a target row
   height, and its flex-grow is that same ratio. Flexbox distributes the
   leftover space in a row in proportion to flex-grow, so every item in
   a row is scaled by the SAME factor — which lands them all on one
   height while each keeps its own width. That is the definition of a
   justified row, in CSS, with no measuring pass and no JavaScript.

   --ar comes from the database, per photograph. It is data, not style. */
.jgal {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  gap: clamp(0.5rem, 0.9vw, 0.85rem);
  /* The height a row aims for before it is stretched to fill the width.
     Tuned against this set's 923x2000 phone screenshots, which are the
     hardest shape here: at 0.46:1 a row height of 230 puts them at 106px
     wide, just clear of the min-width floor below. Lower, and the floor
     engages and the row stops being justified — measured at 768px with
     --row 200: one row held items 200 and 260 tall. */
  --row: clamp(230px, 26vw, 320px);
}

/* THE LAST ROW MUST NOT STRETCH. Without this the final row's one or
   two photographs are blown up to fill the width — a single landscape
   at the end would render three times the height of everything above
   it. A greedy spacer takes the slack instead, so the last row keeps
   the target height and simply ends early. */
.jgal::after {
  content: "";
  flex-grow: 999999;
}

.jgal-item {
  flex-grow: var(--ar);
  flex-basis: calc(var(--ar) * var(--row));
  /* A floor, so a very tall picture is still a picture and not a
     vertical line. It costs that row its justification when it engages,
     so --row above is set high enough that it does not — this is the
     safety net for a shape nobody has uploaded yet, not the mechanism. */
  min-width: min(104px, 100%);
  margin: 0;
}

.jgal-item figure { margin: 0; height: 100%; }

.jgal-link {
  display: block;
  border-radius: var(--radius);
  overflow: hidden;
  background: color-mix(in oklab, var(--ink) 6%, transparent);
  /* The item's own shape. object-fit is deliberately NOT cover: the
     box already matches the picture, so there is nothing to crop. */
  aspect-ratio: var(--ar);
}
.jgal-link:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 3px; }

.jgal-link img,
.jgal-link picture {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  transition: transform 500ms cubic-bezier(0.2, 0, 0.2, 1), opacity 200ms ease;
}
.jgal-link:hover img { transform: scale(1.02); opacity: 0.94; }

.jgal-item figcaption {
  margin-top: 0.5rem;
  font-size: 0.82rem;
  line-height: 1.45;
  color: color-mix(in oklab, var(--ink) 72%, transparent);
}

@media (max-width: 34rem) {
  /* One picture at a time on a phone. Justified rows on a 350px column
     put two portraits side by side at 90px each, which is a contact
     sheet, not a gallery. */
  .jgal { --row: 60vh; }
  .jgal-item { flex-basis: 100%; min-width: 100%; }
  .jgal::after { display: none; }
}

/* ── Foot ───────────────────────────────────────────────────────────*/
.alb-foot {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 1.25rem 2rem;
  margin-top: clamp(2.5rem, 5vw, 4rem);
  padding-top: 1.5rem;
  padding-bottom: clamp(2rem, 4vw, 3rem);
  border-top: var(--hairline-ink);
}
.alb-back { font-weight: 600; text-decoration: none; }
.alb-back:hover { text-decoration: underline; text-underline-offset: 3px; }

.alb-next {
  display: block;
  text-align: right;
  text-decoration: none;
  color: inherit;
  max-width: 26rem;
}
.alb-next-label {
  display: block;
  font-size: var(--text-eyebrow);
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: color-mix(in oklab, var(--ink) 65%, transparent);
}
.alb-next-title {
  display: block;
  margin-top: 0.2rem;
  font-size: 1.25rem;
  line-height: 1.2;
}
.alb-next:hover .alb-next-title { color: var(--jj-500); }

@media (max-width: 34rem) {
  .alb-next { text-align: left; }
}

/* ── Lightbox ───────────────────────────────────────────────────────
   A native <dialog>. The top layer, the backdrop, focus trapping and
   Escape all come from the platform, which is why this is a handful of
   rules rather than a library. */
.lb {
  /* Named once and used by both the stage's padding and the image's
     ceiling below. Two numbers that must agree are one number. */
  --lb-pad: clamp(1rem, 4vw, 3.5rem);
  --lb-bar: 4.5rem;
  width: 100vw;
  max-width: 100vw;
  height: 100dvh;
  max-height: 100dvh;
  margin: 0;
  padding: 0;
  border: 0;
  /* OPAQUE. At 0.96 the album title showed through the picture — a
     ghost of the page behind a photograph reads as a rendering fault,
     not as depth. The ::backdrop below still covers what the dialog
     does not, on browsers that inset it. */
  background: #021414;
  color: #fff;
  overflow: hidden;
}
.lb::backdrop { background: rgb(2 20 20 / 0.9); }

.lb-stage {
  position: absolute;
  inset: 0 0 var(--lb-bar);
  display: grid;
  place-items: center;
  padding: var(--lb-pad);
}

.lb-img {
  display: block;
  /* VIEWPORT MATH, NOT PERCENTAGES. max-height: 100% on a centred grid
     item hits a cyclic size dependency and falls back to `none` — the
     923x2000 screenshot rendered at its full natural height inside an
     828px stage, overflowing it by 1172px, with the computed style
     still reading "100%". The identical trap ate the client logo
     previews in the admin. A length that resolves against the viewport
     cannot fail this way. */
  max-width: calc(100vw - 2 * var(--lb-pad));
  max-height: calc(100dvh - var(--lb-bar) - 2 * var(--lb-pad));
  /* CONTAIN, NOT COVER. The point of opening a photograph full screen is
     to see the whole of it. */
  object-fit: contain;
  border-radius: var(--radius);
}

.lb-bar {
  position: absolute;
  inset: auto 0 0 0;
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 1.5rem;
  min-height: var(--lb-bar);
  padding: 1rem var(--lb-pad);
}
.lb-cap {
  margin: 0;
  font-size: 0.92rem;
  line-height: 1.5;
  max-width: 60ch;
  color: rgb(255 255 255 / 0.88);
}
.lb-count {
  margin: 0;
  flex: none;
  font-size: 0.82rem;
  letter-spacing: 0.08em;
  color: rgb(255 255 255 / 0.7);
}

.lb-btn {
  position: absolute;
  display: grid;
  place-items: center;
  /* 44px, the tap-target floor. The icon inside is 24. */
  width: 44px;
  height: 44px;
  border: 1px solid rgb(255 255 255 / 0.22);
  border-radius: 999px;
  background: rgb(255 255 255 / 0.08);
  color: #fff;
  cursor: pointer;
  transition: background-color 140ms ease, border-color 140ms ease;
}
.lb-btn:hover { background: rgb(255 255 255 / 0.18); border-color: rgb(255 255 255 / 0.4); }
.lb-btn:focus-visible { outline: 2px solid #fff; outline-offset: 2px; }
.lb-btn[hidden] { display: none; }

.lb-prev  { top: calc(50% - var(--lb-bar) / 2); left: clamp(0.5rem, 2vw, 1.5rem); transform: translateY(-50%); }
.lb-next  { top: calc(50% - var(--lb-bar) / 2); right: clamp(0.5rem, 2vw, 1.5rem); transform: translateY(-50%); }
.lb-close { top: clamp(0.5rem, 2vw, 1.5rem); right: clamp(0.5rem, 2vw, 1.5rem); }

@media (prefers-reduced-motion: reduce) {
  .jgal-link img { transition: none; }
  .jgal-link:hover img { transform: none; }
  .lb-btn { transition: none; }
}

/* ── The rate board ─────────────────────────────────────────────────
   Sits between the masthead and the front page. Gold, because on this
   site gold marks what is measured, dated or priced, and this is all
   three — it is the only element on the page a reader might act on with
   their own money.

   The whole band is quiet on purpose. It is a reference, consulted and
   left; styled as loudly as the news it would fight the lead. */
.rates {
  border-block: var(--hairline-ink);
  background: color-mix(in oklab, var(--ink) 3%, transparent);
}
.rates-in { padding-block: 1rem 1.1rem; }

.rates-head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: 0.35rem 1.5rem;
  margin-bottom: 0.85rem;
}

.rates-h a {
  color: inherit;
  text-decoration: none;
  border-bottom: 1px solid color-mix(in oklab, var(--gold) 45%, transparent);
}
.rates-h a:hover { border-bottom-color: var(--gold); }

.rates-h {
  margin: 0;
  font-family: var(--font-body);
  font-size: var(--text-eyebrow);
  font-weight: 700;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--gold);
}

.rates-when {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.3rem 0.7rem;
  margin: 0;
  font-size: 0.8rem;
  color: color-mix(in oklab, var(--ink) 65%, transparent);
}

/* Out of date is stated, not implied by a subtle tint. FENEGOSIDA does
   not publish on Saturdays or holidays, so a day-old rate is normal and
   is NOT flagged; past two days it says so in words. */
.rates.is-stale { background: color-mix(in oklab, #98111F 5%, transparent); }
.rates-when strong {
  padding: 0.1rem 0.5rem;
  border-radius: 999px;
  background: #98111F;
  color: #fff;
  font-size: 0.72rem;
  letter-spacing: 0.04em;
}

.rates-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: 0.75rem 2rem;
  grid-template-columns: repeat(auto-fit, minmax(min(15rem, 100%), 1fr));
}

.rate {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.3rem 0.7rem;
  padding-block: 0.35rem;
  border-top: var(--hairline-ink);
}

.rate-metal { display: flex; flex-direction: column; min-width: 6.5rem; }
/* Nepali leads. A rate board in a Kathmandu shop is read in Devanagari
   first, and Mukta is in the body stack so the line does not jump. */
.rate-ne { font-size: 0.95rem; font-weight: 600; line-height: 1.25; }
.rate-en {
  /* 12px floor. 0.72rem is 11.5. */
  font-size: var(--text-eyebrow);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: color-mix(in oklab, var(--ink) 65%, transparent);
}

.rate-fig {
  margin-left: auto;
  text-align: right;
  font-size: 1.15rem;
  font-weight: 700;
  letter-spacing: -0.01em;
}
.rate-cur {
  margin-right: 0.15rem;
  font-size: 0.72em;
  font-weight: 600;
  color: color-mix(in oklab, var(--ink) 65%, transparent);
}
/* max(), so the suffix never falls under 12px beside a small figure —
   the same rule the marketing site's .unit carries. */
.rate-fig .unit {
  font-size: max(0.62em, 0.75rem);
  font-weight: 500;
  color: color-mix(in oklab, var(--ink) 65%, transparent);
}

/* The gram figure sits under the tola one, quieter, so the board still
   answers "how much is a tola" in one glance. */
.rate-10g {
  display: block;
  margin-top: 0.1rem;
  font-size: 0.78rem;
  font-weight: 500;
  color: color-mix(in oklab, var(--ink) 65%, transparent);
}
.rate-10g .unit { font-size: max(0.85em, 0.75rem); }

.rate-move {
  flex: none;
  min-width: 4.5rem;
  text-align: right;
  font-size: 0.82rem;
  font-weight: 600;
}
/* Both measured on the showcase ground: 4.63:1 and 5.06:1. The obvious
   green and red — #22A45D and #E53935 — are 2.4 and 3.3, and a figure
   somebody may act on is the last place to accept a decorative colour
   that cannot be read. */
.rate-move.is-up { color: #12703F; }
.rate-move.is-down { color: #A31220; }
.rate-move.is-flat {
  font-weight: 500;
  color: color-mix(in oklab, var(--ink) 65%, transparent);
}

.rates-src {
  margin: 0.8rem 0 0;
  font-size: 0.75rem;
  color: color-mix(in oklab, var(--ink) 65%, transparent);
}

@media (max-width: 30rem) {
  .rate-fig { font-size: 1.05rem; }
  .rate-move { min-width: 0; }
}

/* ── Headline thumbnails ────────────────────────────────────────────
   The column was a wall of type beside a large photograph, which is the
   one place a front page reads as unfinished. */
.fp-item-link { grid-template-columns: auto auto 1fr; }

.fp-item-thumb {
  display: block;
  flex: none;
  width: 4.5rem;
  border-radius: var(--radius);
  overflow: hidden;
  background: color-mix(in oklab, var(--ink) 6%, transparent);
}
.fp-item-thumb img,
.fp-item-thumb picture,
.fp-item-thumb .plate {
  display: block;
  width: 100%;
  aspect-ratio: 4 / 3;
  object-fit: cover;
}
/* The plate's letter is sized for a card; at 72px it needs to come down
   or it fills the tile edge to edge. */
.fp-item-thumb .plate-glyph { font-size: 1.5rem; }

@media (max-width: 30rem) {
  /* Under 480px the number, the picture and a headline cannot all have
     room. THE NUMBER GOES, NOT THE PICTURE — the figures are a spine for
     a column of type, and on a phone the pictures are doing that job
     instead. Measured at 360px: 234px left for the headline. */
  .fp-item-n { display: none; }
  .fp-item-thumb { width: 4rem; }
  .fp-item-link { grid-template-columns: auto 1fr; gap: 0.8rem; }
}

/* ── The diary's pictures ───────────────────────────────────────────*/
.diary-row { grid-template-columns: auto 1fr auto auto; }

.diary-pic {
  display: block;
  flex: none;
  width: clamp(6rem, 14vw, 11rem);
  border-radius: var(--radius);
  overflow: hidden;
  background: rgb(255 255 255 / 0.06);
}
.diary-pic img,
.diary-pic picture {
  display: block;
  width: 100%;
  aspect-ratio: 16 / 10;
  object-fit: cover;
}
.diary-pic:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 3px; }

@media (max-width: 44rem) {
  /* The date block, the words, then the picture underneath — a 5rem
     thumbnail beside a wrapped title leaves neither enough room. */
  .diary-row { grid-template-columns: auto 1fr; }
  .diary-pic { grid-column: 2; width: 100%; margin-top: 0.6rem; }
  .diary-pic img, .diary-pic picture { aspect-ratio: 21 / 9; }
}

/* ── The home strip of photographs ──────────────────────────────────
   The same justified mechanism as the album page — see .jgal — at a
   shallower row, because this is a taste of the galleries rather than
   one of them. */
/* --row AND the floor have to agree, or the rows stop being justified.
   At 150px the 0.46:1 screenshots land at 69px, under the album page's
   104px floor, so the floor engaged and one row measured items 225 and
   219 tall. A shallower strip needs a shallower floor to match. */
.jgal--home { --row: clamp(160px, 15vw, 230px); }
.jgal--home .jgal-item { min-width: min(64px, 100%); }

.jgal-link { position: relative; }

.jgal-cap {
  position: absolute;
  inset: auto 0 0 0;
  padding: 1.75rem 0.7rem 0.6rem;
  font-size: 0.78rem;
  font-weight: 600;
  line-height: 1.25;
  color: #fff;
  /* A gradient, not a flat panel: the photograph darkens into its own
     caption instead of wearing a bar. Same device as the album tiles. */
  background: linear-gradient(to top,
    rgb(2 25 25 / 0.86) 0%,
    rgb(2 25 25 / 0.55) 45%,
    rgb(2 25 25 / 0) 100%);
  opacity: 0;
  transition: opacity 180ms ease;
}
/* Shown on hover and on focus. Always visible it would paint a caption
   across the foot of eight photographs at once, which is a lot of
   furniture for one piece of information; :focus-within is what keeps
   it reachable without a mouse. */
.jgal-link:hover .jgal-cap,
.jgal-link:focus-visible .jgal-cap { opacity: 1; }

/* A touch screen has no hover, so the caption would never appear. */
@media (hover: none) {
  .jgal-cap { opacity: 1; }
}

@media (max-width: 34rem) {
  /* The album page gives a phone one picture per row. Eight of those
     between the diary and the footer is a gallery, not a taste of one,
     so the strip stays justified here and shows FOUR. The rest are one
     tap away under All albums — this is a teaser, and the section header
     says where the others live. */
  .jgal--home { --row: 150px; }
  .jgal--home .jgal-item { flex-basis: calc(var(--ar) * var(--row)); }
  .jgal--home .jgal-item:nth-child(n+5) { display: none; }
  .jgal--home::after { display: block; }
}

@media (prefers-reduced-motion: reduce) {
  .jgal-cap { transition: none; }
}

/* ============================================================
   The rate history page
   ============================================================
   ONE CHART PER METAL, NEVER TWO SCALES ON ONE PLOT. Gold sits near
   Rs 305,000 a tola and silver near Rs 4,800 — sixty times apart. A
   second y-axis would let them share a plot, and the alignment of the
   two scales would be arbitrary: the chart would invent a correlation
   that is not in the data. Two measures of different magnitude get two
   charts.

   Each chart carries ONE series, so there is no legend box — the
   heading names it.

   THE MARK COLOURS ARE NOT THE BRAND TEXT TOKENS. --gold (#836919) and
   --jj-500 (#034848) are built for type on light: run through the
   contrast-and-chroma checks as chart marks they fail the lightness
   band and the chroma floor — #034848 comes out at L 0.36 and chroma
   0.061, which is to say it reads as grey once it is a 2px line rather
   than a letterform. #A87C15 and #00938A are the same two hues stepped
   to where a thin mark reads, and they pass all six checks against a
   white surface: lightness band, chroma floor, CVD separation
   (ΔE 13.0 protan), normal-vision separation (18.0) and 3:1 contrast.
   Re-run the checks before changing either.
   ============================================================ */

.rate-head { padding-block: var(--page-top) 0; margin-bottom: clamp(1.5rem, 3vw, 2.5rem); }
.rate-title { margin: 0; font-size: clamp(2rem, 4.4vw, 3.25rem); line-height: 1.05; letter-spacing: -0.022em; }
.rate-lede {
  max-width: 54ch;
  margin: 1rem 0 0;
  font-size: clamp(1.05rem, 1.3vw, 1.2rem);
  line-height: 1.55;
  color: color-mix(in oklab, var(--ink) 72%, transparent);
}

.rate-grid {
  display: grid;
  gap: clamp(1.25rem, 2.5vw, 2rem);
  padding-bottom: clamp(2rem, 4vw, 3rem);
}
@media (min-width: 62rem) {
  .rate-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

.rcard {
  min-width: 0;
  padding: clamp(1.1rem, 2vw, 1.6rem);
  border: var(--hairline-ink);
  border-radius: 12px;
  background: #fff;
}

/* The mark colour rides on `color`, so the line, the area gradient
   (currentColor), the dot and the crosshair all take it from one
   declaration. */
.rcard { --mark: #A87C15; color: var(--ink); }
.rcard:has([data-tone="silver"]) { --mark: #00938A; }

.rcard-head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: 0.5rem 1.25rem;
}

.rcard-h { margin: 0; font-family: var(--font-body); }
.rcard-ne { display: block; font-size: 1.05rem; font-weight: 700; line-height: 1.2; }
.rcard-en {
  display: block;
  margin-top: 0.1rem;
  font-size: var(--text-eyebrow);
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: color-mix(in oklab, var(--ink) 65%, transparent);
}

/* The stat tile. PROPORTIONAL DIGITS, not tabular: at this size equal
   width digits make a figure look loose. tabular-nums is for columns
   that have to line up, which is the table below and the axis ticks. */
.rcard-now { margin: 0; white-space: nowrap; }
.rcard-cur {
  font-size: 0.85rem;
  font-weight: 600;
  color: color-mix(in oklab, var(--ink) 65%, transparent);
  margin-right: 0.15rem;
}
.rcard-fig { font-size: clamp(1.5rem, 2.4vw, 2rem); font-weight: 700; letter-spacing: -0.02em; }
.rcard-unit {
  font-size: max(0.8rem, 0.75rem);
  font-weight: 500;
  color: color-mix(in oklab, var(--ink) 65%, transparent);
}

.rcard-sub {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.3rem 1rem;
  margin: 0.5rem 0 0;
  font-size: 0.85rem;
  color: color-mix(in oklab, var(--ink) 65%, transparent);
}

/* Direction, measured on this ground: 4.63:1 and 5.06:1. The obvious
   green and red are 2.4 and 3.3 — a figure a reader may act on is the
   last place for a decorative colour that cannot be read. */
.rmove { font-weight: 600; }
.rmove.is-up { color: #12703F; }
.rmove.is-down { color: #A31220; }
.rmove.is-flat { font-weight: 500; }
.rmove-since { font-weight: 500; color: color-mix(in oklab, var(--ink) 65%, transparent); }

/* ── The chart ───────────────────────────────────────────────────── */
.rchart { margin: 1.1rem 0 0; }

.rchart-plot {
  position: relative;
  width: 100%;
  /* A FIXED HEIGHT, NOT AN ASPECT RATIO, because the chart is drawn at
     the container's real pixel width now (see rates.js) — one user unit
     is one CSS pixel, so the box must not scale. The 260 includes the
     x-axis band: a container sized to the plot alone crops its own date
     labels and grows a little nested scrollbar. */
  height: 260px;
  cursor: crosshair;
  touch-action: pan-y;
}
@media (max-width: 34rem) {
  /* Shorter on a phone, where 260px of chart in a 740px viewport pushes
     the second metal off the screen entirely. */
  .rchart-plot { height: 210px; }
}
.rchart-plot:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 4px; border-radius: 6px; }

.rchart-svg { display: block; width: 100%; height: 100%; overflow: visible; color: var(--mark); }

/* Recessive chrome. SOLID hairlines: a dashed grid reads as a
   threshold when it is only a ruler. */
.rc-grid { stroke: color-mix(in oklab, var(--ink) 10%, transparent); stroke-width: 1; }
.rc-ytick,
.rc-xtick {
  font-family: var(--font-body);
  /* 12px, the site's floor, and it now MEANS 12px — while the viewBox
     was a fixed 720 units this rendered at 9.7px on a laptop and 4.3px
     on a 360px phone. */
  font-size: 12px;
  font-variant-numeric: tabular-nums;
  fill: color-mix(in oklab, var(--ink) 60%, transparent);
}
.rc-ytick { text-anchor: end; }

.rc-area { opacity: 0; }
/* Without JavaScript nothing draws, so nothing here may depend on the
   animation having run — but the chart is only built by the script in
   the first place, and the table is the no-JS route to the figures. */
.rchart.is-drawn .rc-area { opacity: 1; }

.rc-line {
  fill: none;
  stroke: var(--mark);
  /* 2px. Thin marks; a heavy stroke on a value line reads as a border. */
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
}

/* Each reading, while there are few enough to count. */
.rc-node { fill: var(--mark); opacity: 0.55; }

/* The endpoint. A surface ring rather than a drawn border, so the dot
   separates from the area beneath it without gaining an outline. */
.rc-halo { fill: #fff; }
.rc-dot { fill: var(--mark); }

.rc-cross,
.rc-mark { opacity: 0; transition: opacity 120ms ease; }
.rc-cross { stroke: color-mix(in oklab, var(--ink) 30%, transparent); stroke-width: 1; }
.rc-mark { fill: var(--mark); stroke: #fff; stroke-width: 2; }
.rchart-svg.is-reading .rc-cross,
.rchart-svg.is-reading .rc-mark { opacity: 1; }

/* [hidden] IS A UA RULE AND AUTHOR RULES BEAT IT. `display: flex` below
   overrode it, so every chart painted an empty black tooltip in its top
   corner from the moment it loaded — visible in the very first render.
   Any element hidden with .hidden AND given a display needs this pair. */
.rc-tip[hidden] { display: none; }

.rc-tip {
  position: absolute;
  top: 0.25rem;
  display: flex;
  flex-direction: column;
  gap: 0.1rem;
  padding: 0.5rem 0.7rem;
  border-radius: 8px;
  background: var(--jj-900);
  color: #fff;
  font-size: 0.8rem;
  line-height: 1.35;
  white-space: nowrap;
  pointer-events: none;
  box-shadow: 0 6px 20px rgb(2 42 42 / 0.22);
  z-index: 2;
}
.rc-tip strong { font-size: 0.92rem; font-variant-numeric: tabular-nums; }
.rc-tip span { font-variant-numeric: tabular-nums; color: rgb(255 255 255 / 0.82); }
.rc-tip-when { color: rgb(255 255 255 / 0.7) !important; }

.rchart-wait {
  margin: 1.1rem 0 0;
  padding: 1.5rem 1rem;
  border: 1px dashed color-mix(in oklab, var(--ink) 18%, transparent);
  border-radius: 10px;
  text-align: center;
  font-size: 0.88rem;
  color: color-mix(in oklab, var(--ink) 65%, transparent);
}

/* ── The table twin ──────────────────────────────────────────────── */
.rtable { margin-top: 1rem; }
.rtable summary {
  cursor: pointer;
  font-size: 0.85rem;
  font-weight: 600;
  color: var(--jj-500);
  padding: 0.35rem 0;
}
.rtable summary:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 2px; border-radius: 4px; }
.rtable[open] summary { margin-bottom: 0.6rem; }
.rtable table { width: 100%; border-collapse: collapse; font-size: 0.85rem; }
.rtable th, .rtable td {
  padding: 0.45rem 0.6rem;
  border-bottom: 1px solid color-mix(in oklab, var(--ink) 8%, transparent);
  text-align: left;
}
.rtable th {
  font-size: var(--text-eyebrow);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: color-mix(in oklab, var(--ink) 65%, transparent);
}
.rtable .right { text-align: right; }
.rtable .sub { display: block; font-size: 0.75rem; color: color-mix(in oklab, var(--ink) 65%, transparent); }

.rate-note {
  margin: 0 0 clamp(2rem, 4vw, 3rem);
  padding: 0.8rem 1rem;
  border-left: 3px solid var(--gold);
  background: color-mix(in oklab, var(--ink) 3%, transparent);
  font-size: 0.88rem;
  color: color-mix(in oklab, var(--ink) 72%, transparent);
}

@media (prefers-reduced-motion: reduce) {
  .rc-cross, .rc-mark { transition: none; }
}

/* ============================================================
   The event page
   ============================================================
   AN EVENT PAGE ANSWERS WHAT, WHEN AND WHERE BEFORE IT DOES ANYTHING
   ELSE — and then gives the reader the two things they came to do: put
   it in their calendar and find their way there.

   The old page led with the title, then a <dl> of when/where, then the
   summary, then the picture, then the body. The answer was assembled
   from three places, and NOTHING ON IT SAID WHETHER THE EVENT HAD
   ALREADY HAPPENED — last year's meeting read exactly like next week's.
   ============================================================ */

/* ONE COLUMN, ONE LEFT EDGE, ONE RIGHT EDGE.
   This page was four widths stacked: a 52rem header, a full-width fact
   panel, a 62rem cover and a 44rem body. Every block began somewhere
   different and the edge stepped in and out down the page — the same
   fault the article page had, and the same fix: --art-measure for all
   of it, so the picture and the words share their edges.
   Everything below that sets a width uses this token. */
.ev-head,
.ev-facts,
.ev-cover,
.ev-body,
.ev-shots,
.ev-foot {
  width: 100%;
  max-width: var(--art-measure);
  margin-inline: auto;
}

.ev-head { padding-block: var(--page-top) 0; }

/* Status first. It is a small badge rather than a line of prose because
   the reader is scanning for it, not reading it. */
.ev-status {
  display: inline-flex;
  align-items: baseline;
  gap: 0.5rem;
  margin: 0 0 0.9rem;
  padding: 0.3rem 0.75rem;
  border-radius: 999px;
  font-size: 0.78rem;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}
/* jj-500 on its own 10% tint, and #12703F / ink measured on theirs —
   a status people rely on is not the place for a colour that fails. */
.ev-status.is-next { background: color-mix(in oklab, var(--jj-500) 12%, transparent); color: var(--jj-500); }
.ev-status.is-now  { background: color-mix(in oklab, #12703F 14%, transparent); color: #0E5A33; }
.ev-status.is-past {
  background: color-mix(in oklab, var(--ink) 8%, transparent);
  color: color-mix(in oklab, var(--ink) 70%, transparent);
}
.ev-status-in {
  font-weight: 500;
  letter-spacing: 0.02em;
  text-transform: none;
  opacity: 0.85;
}

.ev-title {
  margin: 0;
  font-size: clamp(1.9rem, 4vw, 3rem);
  line-height: 1.06;
  letter-spacing: -0.022em;
}

.ev-lede {
  max-width: 54ch;
  margin: 1rem 0 0;
  font-size: clamp(1.05rem, 1.3vw, 1.2rem);
  line-height: 1.55;
  color: color-mix(in oklab, var(--ink) 72%, transparent);
}

/* ── The fact panel ──────────────────────────────────────────────────
   When, where and the actions in ONE block. They are one answer, and
   the actions belong beside them because that is the moment a reader
   decides whether to go. */
.ev-facts {
  display: grid;
  gap: 1px;
  margin-block: clamp(1.5rem, 3vw, 2.25rem) clamp(1.75rem, 3vw, 2.5rem);
  /* The gap IS the rule: a 1px grid gap over a hairline ground draws
     the dividers, so no cell needs a border that then has to be
     un-drawn on the last one. */
  background: color-mix(in oklab, var(--ink) 12%, transparent);
  border: var(--hairline-ink);
  border-radius: 12px;
  overflow: hidden;
}
@media (min-width: 48rem) {
  .ev-facts { grid-template-columns: repeat(auto-fit, minmax(min(15rem, 100%), 1fr)); }
}

.ev-fact { background: #fff; padding: clamp(0.9rem, 1.8vw, 1.25rem); }

.ev-fact-k {
  margin: 0 0 0.4rem;
  font-size: var(--text-eyebrow);
  font-weight: 700;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: color-mix(in oklab, var(--ink) 65%, transparent);
}
.ev-fact-v { margin: 0; font-size: 0.95rem; line-height: 1.5; }

/* The Nepali date leads and is the one set in ink, as everywhere else
   on this site; the Gregorian date and the time follow, quieter. */
.ev-when-bs { display: block; font-weight: 700; font-size: 1.05rem; }
.ev-when-ad { display: block; color: color-mix(in oklab, var(--ink) 72%, transparent); }
.ev-when-t { display: block; margin-top: 0.15rem; color: color-mix(in oklab, var(--ink) 72%, transparent); }

.ev-place { display: block; font-weight: 600; }
.ev-map { display: inline-block; margin-top: 0.35rem; font-size: 0.88rem; font-weight: 600; text-decoration: none; }
.ev-map:hover { text-decoration: underline; text-underline-offset: 3px; }

/* The action cell. Filled, because it is the one thing on this panel
   that DOES something rather than telling you something. */
.ev-fact--do { display: flex; flex-direction: column; justify-content: center; gap: 0.4rem; }

.ev-cal {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.5rem;
  padding: 0.7rem 1.1rem;
  border-radius: 8px;
  background: var(--jj-500);
  color: #fff;
  font-size: 0.95rem;
  font-weight: 600;
  text-decoration: none;
  text-align: center;
  transition: background-color 140ms ease;
}
.ev-cal:hover { background: var(--jj-700); color: #fff; }
.ev-cal:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 3px; }
.ev-cal-note { font-size: 0.78rem; color: color-mix(in oklab, var(--ink) 65%, transparent); text-align: center; }

/* ── Cover and body ──────────────────────────────────────────────────*/
.ev-cover { margin-block: 0 clamp(1.75rem, 3vw, 2.5rem); }
.ev-cover img,
.ev-cover picture {
  display: block;
  width: 100%;
  /* Declared, so the page does not jump as the picture lands. 16:9
     rather than the image's own shape: an event cover is a banner, and
     a 4:3 upload otherwise pushes the account of the event off the
     screen entirely. */
  aspect-ratio: 16 / 9;
  object-fit: cover;
  border-radius: 12px;
  background: color-mix(in oklab, var(--ink) 5%, transparent);
}
.ev-cover figcaption {
  margin-top: 0.6rem;
  font-size: 0.82rem;
  color: color-mix(in oklab, var(--ink) 65%, transparent);
}

/* The account of the event is prose and gets the article page's pair.
   THE SIZE COMES WITH THE MEASURE: 44rem at 18px is about 88 characters
   a line, well past readable — the same arithmetic that put the article
   body at 21px applies here, because it is the same column. */
.ev-body {
  font-size: 1.3125rem;
  line-height: 1.75;
}

/* ── Albums from the event ───────────────────────────────────────────*/
.ev-shots { margin-block: clamp(2.5rem, 5vw, 3.5rem) 0; }

.ev-albums {
  list-style: none;
  margin: 1.5rem 0 0;
  padding: 0;
  display: grid;
  gap: clamp(1rem, 2vw, 1.5rem);
  grid-template-columns: repeat(auto-fill, minmax(min(16rem, 100%), 1fr));
}

.ev-album { display: block; text-decoration: none; color: inherit; }
.ev-album:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 4px; border-radius: 12px; }

.ev-album-pic {
  display: block;
  border-radius: 10px;
  overflow: hidden;
  background: color-mix(in oklab, var(--ink) 6%, transparent);
}
.ev-album-pic img,
.ev-album-pic picture {
  display: block;
  width: 100%;
  aspect-ratio: 4 / 3;
  object-fit: cover;
  transition: transform 400ms cubic-bezier(0.2, 0, 0.2, 1);
}
.ev-album:hover .ev-album-pic img { transform: scale(1.03); }

/* The title sits UNDER the picture here, not over it. On the home strip
   a caption over the image works because those are photographs being
   shown; these are covers standing for a whole album, and the name is
   the point rather than a label on the picture. */
.ev-album-t { display: block; margin-top: 0.6rem; font-size: 1.05rem; line-height: 1.25; }
.ev-album:hover .ev-album-t { color: var(--jj-500); }
.ev-album-n {
  display: block;
  margin-top: 0.15rem;
  font-size: 0.8rem;
  color: color-mix(in oklab, var(--ink) 65%, transparent);
}

/* ── Foot ────────────────────────────────────────────────────────────*/
.ev-foot {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 1.25rem 2rem;
  margin-block: clamp(2.5rem, 5vw, 4rem) 0;
  padding-top: 1.5rem;
  padding-bottom: clamp(2rem, 4vw, 3rem);
  border-top: var(--hairline-ink);
}
.ev-back { font-weight: 600; text-decoration: none; }
.ev-back:hover { text-decoration: underline; text-underline-offset: 3px; }

.ev-next { display: block; max-width: 26rem; text-align: right; text-decoration: none; color: inherit; }
.ev-next-k {
  display: block;
  font-size: var(--text-eyebrow);
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: color-mix(in oklab, var(--ink) 65%, transparent);
}
.ev-next-t { display: block; margin-top: 0.2rem; font-size: 1.2rem; line-height: 1.2; }
.ev-next:hover .ev-next-t { color: var(--jj-500); }

@media (max-width: 34rem) {
  .ev-next { text-align: left; }
}

@media (prefers-reduced-motion: reduce) {
  .ev-cal { transition: none; }
  .ev-album-pic img { transition: none; }
  .ev-album:hover .ev-album-pic img { transform: none; }
}

/* ============================================================
   Motion
   ============================================================
   ALL OF IT IS CSS. NOT ONE BYTE OF JAVASCRIPT IS ADDED.

   Two platform features landed since this site was started and between
   them they cover everything a motion library used to be for:

   - @view-transition gives an ordinary multi-page site real page
     transitions. Three lines, no router, no framework. Chrome 126+ and
     Safari 18.2+; Firefox is still working on it and simply navigates
     the way it always did.

   - animation-timeline: view() ties an animation to an element's own
     journey through the viewport. It runs on the compositor, so a
     reveal costs nothing on the main thread and cannot jank the scroll
     it is reacting to. About 84% of browsers; the rest see the content,
     which is the whole point of the guard below.

   THREE RULES HOLD THIS UP.

   1. CONTENT IS NEVER GATED BY ANIMATION. Every reveal lives inside
      @supports (animation-timeline: view()). Where that is not
      supported the rule never applies, nothing is ever set to opacity
      0, and the page is simply a page. There is no state in which a
      reader waits for something to fade in that never will.

   2. TRANSFORM AND OPACITY ONLY. Never top, left, width, height or
      margin — those relayout every frame. These two are composited.

   3. prefers-reduced-motion IS OBEYED EVERYWHERE, including by the view
      transitions, which are otherwise the one thing that would keep
      moving after the reader has asked it not to.

   WHAT IS DELIBERATELY NOT HERE: scroll hijacking. A library that takes
   over the wheel to add inertia (Lenis and friends) breaks find-in-page,
   fights the trackpad's own momentum, makes keyboard scrolling feel
   broken and adds weight to every page. Native scroll-behavior does the
   part that actually helps — anchors — and nothing else is touched.
   ============================================================ */

/* ── Page transitions ────────────────────────────────────────────────
   Both documents need this rule, which on this site is automatic: one
   stylesheet serves every page. */
@view-transition { navigation: auto; }

/* The header keeps its own identity across the navigation. Without a
   name it is part of the root snapshot and cross-fades with everything
   else — the same bar fading into an identical copy of itself, which
   reads as a flicker exactly where the eye is resting. Named, it is
   matched old-to-new and simply stays put.

   A view-transition-name must be UNIQUE IN THE DOCUMENT. These are the
   page frame, so there is one of each. */
.site-head { view-transition-name: site-head; }
.site-foot { view-transition-name: site-foot; }

@media (prefers-reduced-motion: no-preference) {
  /* The outgoing page leaves faster than the incoming one arrives:
     equal timings read as a dissolve, which is slower to the eye than
     the two overlapping. */
  ::view-transition-old(root) {
    animation: vt-leave 160ms cubic-bezier(0.4, 0, 1, 1) both;
  }
  ::view-transition-new(root) {
    animation: vt-arrive 300ms cubic-bezier(0.16, 0.84, 0.44, 1) both;
  }

  /* The frame does not move at all. */
  ::view-transition-group(site-head),
  ::view-transition-group(site-foot) { animation-duration: 0s; }
}

@keyframes vt-leave {
  to { opacity: 0; transform: translateY(-8px); }
}
@keyframes vt-arrive {
  from { opacity: 0; transform: translateY(12px); }
}

/* A transition is motion, and a reader who has asked for none has asked
   for none. Killed rather than shortened: a 1ms cross-fade is still a
   cross-fade. */
@media (prefers-reduced-motion: reduce) {
  ::view-transition-group(*),
  ::view-transition-old(*),
  ::view-transition-new(*) { animation: none !important; }
}

/* ── Anchors ─────────────────────────────────────────────────────────
   The one piece of smooth scrolling worth having, and the browser's
   own. scroll-padding keeps a landing clear of the sticky header —
   without it an anchor puts its target underneath the bar. */
@media (prefers-reduced-motion: no-preference) {
  :root { scroll-behavior: smooth; }
}
:root { scroll-padding-top: 5.5rem; }

/* ── Reveals ─────────────────────────────────────────────────────────
   Tied to each element's own passage through the viewport, so the
   timing is the reader's scroll rather than a guess about it.

   @supports IS THE SAFETY. Everything that sets opacity: 0 is inside
   it, so a browser without scroll timelines never applies the rule and
   never has anything to reveal.

   Selectors are the existing blocks — no markup was touched to add
   this, and nothing has to be remembered when a new page is built. */
@media (prefers-reduced-motion: no-preference) {
  @supports (animation-timeline: view()) {
    .fp-lead,
    .fp-more,
    .rcard,
    .diary-row,
    .jgal-item,
    .post,
    .eventrow,
    .ev-facts,
    .ev-cover,
    .ev-body,
    .ev-albums > li,
    .alb-meta,
    .rate-grid > *,
    .art-more .post {
      animation: rise-in linear both;
      animation-timeline: view();
      /* Finished well before the element reaches the middle of the
         screen. A reveal still running when the reader is already
         reading it is the thing that makes scroll animation feel
         cheap — this is done at 40% of the entry, which is roughly
         where a block clears the fold. */
      animation-range: entry 0% entry 40%;
    }

    /* The headline column's rows carry a small stagger, and it comes
       from their POSITION rather than a per-item delay: each one is on
       its own timeline, so they arrive in order because they arrive at
       the viewport in order. No nth-child arithmetic to maintain. */
    .fp-item { animation-range: entry 5% entry 45%; }
  }
}

@keyframes rise-in {
  from { opacity: 0; transform: translateY(18px); }
  to   { opacity: 1; transform: none; }
}

/* ── The first paint ─────────────────────────────────────────────────
   ONE short rise on the masthead and the lead, so a cold load has the
   same manners as a navigation. A time-based animation rather than a
   scroll-driven one, because at load there has been no scroll — and it
   only ever animates TO the resting state, so an interrupted load still
   ends with a readable page.

   NOT A PRELOADER. A splash screen that holds the content back to show
   a spinner makes the site slower to the only measure that counts. */
@media (prefers-reduced-motion: no-preference) {
  .masthead-id,
  .rate-head,
  .alb-head,
  .ev-head,
  .page-open { animation: enter 520ms cubic-bezier(0.16, 0.84, 0.44, 1) both; }

  .masthead-meta { animation: enter 520ms cubic-bezier(0.16, 0.84, 0.44, 1) 80ms both; }
}

@keyframes enter {
  from { opacity: 0; transform: translateY(10px); }
  to   { opacity: 1; transform: none; }
}

/* ── Micro-interactions ──────────────────────────────────────────────
   Small, and on the things that respond to a press. */
@media (prefers-reduced-motion: no-preference) {
  .btn,
  .ev-cal,
  .rtable summary,
  .site-nav a,
  .more { transition: transform 140ms cubic-bezier(0.2, 0, 0.2, 1), color 140ms ease, background-color 140ms ease; }

  /* :active, not :hover — a link that shifts under the cursor is a link
     that is harder to click. This is the press acknowledging itself. */
  .btn:active,
  .ev-cal:active { transform: translateY(1px); }

  .more:hover { transform: translateX(2px); }

  /* The card lift is on the shadow and a single pixel, not a scale:
     scaling a card resamples its text for the length of the tween. */
  .post,
  .rcard,
  .nextev { transition: transform 180ms cubic-bezier(0.2, 0, 0.2, 1), border-color 180ms ease, box-shadow 180ms ease; }
  .post:hover,
  .rcard:hover { transform: translateY(-2px); box-shadow: 0 8px 26px rgb(2 42 42 / 0.07); }
}

/* ============================================================
   Reactions and comments
   ============================================================
   Deliberately plain. The research on comment sections in 2026 all points
   the same way: the ones that still work have been simplified back to
   text — avatar, name, time, what they said. Vote arrows, badges, gold
   stars and nested threads are what turned comment sections into
   something readers learned to scroll past.

   No threading here either. A jewellers' noticeboard has conversations
   five comments long, and a reply tree costs indentation, a collapsed
   state and a "load more" before it earns anything.
   ============================================================ */

/* THE PUBLIC STYLESHEET HAD NO .sr-only. It exists in admin.css, which
   is a different file that this page never loads — so every visually
   hidden label printed as ordinary text, and each reaction button read
   "👍 Sign in to react: Useful" across the row. A class that is missing
   rather than wrong fails loudly in exactly this way. */
.sr-only {
  position: absolute;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

.engage {
  margin-top: clamp(3rem, 6vw, 4.5rem);
  padding-block: clamp(2.5rem, 5vw, 3.5rem);
  border-top: 1px solid color-mix(in oklab, var(--ink) 12%, transparent);
}

/* Same measure as the article. The conversation is part of the reading
   column, not a wider block bolted underneath it. */
.engage-col {
  width: 100%;
  /* Same arithmetic as .longform, for the same reason and so the two
     land on the same left edge. Capping the padded box put the
     conversation 77px inside the article it belongs to at 1920. */
  max-width: calc(var(--art-measure) + 2 * var(--gutter));
  margin-inline: auto;
  padding-inline: var(--gutter);
}

.engage-h {
  margin: 0 0 1.1rem;
  font-family: var(--font-body);
  font-size: 0.74rem;
  font-weight: 700;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: color-mix(in oklab, var(--ink) 58%, transparent);
}

.engage-flash {
  margin: 0 0 1.25rem;
  padding: 0.7rem 0.95rem;
  border-radius: 10px;
  font-size: 0.9rem;
}
.engage-flash.is-ok {
  background: color-mix(in oklab, #0F6E3F 12%, transparent);
  color: #0B5230;
}
.engage-flash.is-bad {
  background: color-mix(in oklab, #98111F 12%, transparent);
  color: #7C0E1A;
}

/* ── Reactions ──────────────────────────────────────────────────── */
.react { margin-bottom: 2.75rem; }

.react-row { display: flex; flex-wrap: wrap; gap: 0.5rem; }
.react-row form { margin: 0; }

.react-btn {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  padding: 0.42rem 0.8rem;
  border: 1px solid color-mix(in oklab, var(--ink) 14%, transparent);
  border-radius: 999px;
  background: #fff;
  font: inherit;
  font-size: 0.9rem;
  line-height: 1.4;
  text-decoration: none;
  color: var(--ink);
  cursor: pointer;
  transition: border-color 130ms ease, background-color 130ms ease, transform 130ms ease;
}
.react-btn:hover {
  border-color: var(--jj-300);
  transform: translateY(-1px);
  color: var(--ink);
}
/* The one you chose. A border and a tint, never colour alone — the
   count beside it changes too, so the state is legible without it. */
.react-btn.is-mine {
  border-color: var(--jj-500);
  background: color-mix(in oklab, var(--jj-500) 8%, #fff);
  font-weight: 600;
}

/* The emoji renders from the system font stack, not the page's. Satoshi
   has no emoji glyphs, so without this the browser falls back per glyph
   and the row sits at inconsistent baselines. */
.react-emoji {
  font-family: "Apple Color Emoji", "Segoe UI Emoji", "Noto Color Emoji", sans-serif;
  font-size: 1.05rem;
  line-height: 1;
}
.react-n {
  font-size: 0.82rem;
  color: color-mix(in oklab, var(--ink) 62%, transparent);
  font-variant-numeric: tabular-nums;
}
.react-btn.is-mine .react-n { color: var(--jj-500); }

@media (prefers-reduced-motion: reduce) {
  .react-btn { transition: none; }
  .react-btn:hover { transform: none; }
}

/* ── The comment box ────────────────────────────────────────────── */
.talk-me {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  margin-bottom: 0.75rem;
  font-size: 0.9rem;
}
.talk-me-name { font-weight: 600; }
.talk-out { margin-left: auto; }
.talk-out button {
  border: 0;
  background: transparent;
  font: inherit;
  font-size: 0.82rem;
  color: color-mix(in oklab, var(--ink) 60%, transparent);
  text-decoration: underline;
  text-underline-offset: 2px;
  cursor: pointer;
  padding: 0.25rem;
}
.talk-out button:hover { color: var(--ink); }

.talk-form textarea {
  width: 100%;
  padding: 0.8rem 0.95rem;
  border: 1px solid color-mix(in oklab, var(--ink) 16%, transparent);
  border-radius: 12px;
  background: #fff;
  font: inherit;
  font-size: 1rem;
  line-height: 1.55;
  resize: vertical;
  min-height: 5.5rem;
}
.talk-form textarea:focus {
  outline: none;
  border-color: var(--jj-500);
  box-shadow: 0 0 0 3px color-mix(in oklab, var(--jj-500) 14%, transparent);
}

.talk-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.75rem;
  margin-top: 0.7rem;
}
.btn-solid {
  display: inline-flex;
  align-items: center;
  padding: 0.6rem 1.25rem;
  border: 0;
  border-radius: 999px;
  background: var(--jj-500);
  color: #fff;
  font: inherit;
  font-size: 0.92rem;
  font-weight: 600;
  cursor: pointer;
}
.btn-solid:hover { background: var(--jj-700); }

.talk-note {
  font-size: 0.78rem;
  line-height: 1.45;
  color: color-mix(in oklab, var(--ink) 58%, transparent);
}

/* ── Signed out ─────────────────────────────────────────────────── */
.talk-gate {
  padding: 1.35rem 1.4rem;
  border: 1px solid color-mix(in oklab, var(--ink) 12%, transparent);
  border-radius: 14px;
  background: color-mix(in oklab, var(--ink) 3%, transparent);
}
.talk-gate > p:first-child {
  margin: 0 0 0.9rem;
  font-size: 1.02rem;
  font-weight: 600;
}
.talk-gate .talk-note { display: block; margin: 0.8rem 0 0; }

/* Google's own button proportions: white ground, its four-colour mark,
   dark text. A teal-branded Google button reads as a phishing page. */
.gbtn {
  display: inline-flex;
  align-items: center;
  gap: 0.65rem;
  padding: 0.6rem 1.15rem;
  border: 1px solid #DADCE0;
  border-radius: 999px;
  background: #fff;
  color: #3C4043;
  font-size: 0.92rem;
  font-weight: 600;
  text-decoration: none;
  box-shadow: 0 1px 2px rgb(60 64 67 / 0.12);
}
.gbtn:hover { background: #F8F9FA; color: #3C4043; border-color: #C9CCD1; }

/* ── The list ───────────────────────────────────────────────────── */
.talk-empty {
  margin: 1.75rem 0 0;
  font-size: 0.95rem;
  color: color-mix(in oklab, var(--ink) 55%, transparent);
}

.talk-list { list-style: none; margin: 2rem 0 0; padding: 0; }

.talk-item {
  display: flex;
  gap: 0.85rem;
  padding: 1.1rem 0;
  border-top: 1px solid color-mix(in oklab, var(--ink) 10%, transparent);
}

.avatar {
  flex: none;
  width: 40px;
  height: 40px;
  border-radius: 50%;
  object-fit: cover;
  background: color-mix(in oklab, var(--jj-500) 12%, transparent);
}
.avatar-ini {
  display: grid;
  place-items: center;
  font-size: 0.85rem;
  font-weight: 700;
  color: var(--jj-500);
  /* Initials are Latin letters; the emoji stack above must not reach
     them or a two-letter monogram renders in a fallback face. */
  font-family: var(--font-body);
}

.talk-body { min-width: 0; flex: 1; }

.talk-head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.6rem;
  margin: 0 0 0.3rem;
}
.talk-who { font-weight: 600; font-size: 0.94rem; }
.talk-when {
  font-size: 0.8rem;
  color: color-mix(in oklab, var(--ink) 55%, transparent);
}

.talk-text {
  font-size: 0.98rem;
  line-height: 1.6;
  /* A pasted URL must not put a scrollbar on the page — the same brake
     the article prose carries, for the same reason. */
  overflow-wrap: break-word;
}

.talk-del {
  margin-top: 0.45rem;
  border: 0;
  background: transparent;
  padding: 0.2rem 0;
  font: inherit;
  font-size: 0.8rem;
  color: color-mix(in oklab, var(--ink) 55%, transparent);
  text-decoration: underline;
  text-underline-offset: 2px;
  cursor: pointer;
}
.talk-del:hover { color: #98111F; }

/* ── The sign-in gate ───────────────────────────────────────────────
   A <dialog>, so focus trapping, Escape and the top layer come from the
   browser rather than from script. It is only ever opened after somebody
   presses Post, so a reader who is only reading never sees it. */
.gate {
  width: min(26rem, calc(100vw - 2rem));
  padding: 1.75rem 1.6rem 1.4rem;
  border: 1px solid color-mix(in oklab, var(--ink) 12%, transparent);
  border-radius: 16px;
  background: #fff;
  color: var(--ink);
  box-shadow: 0 24px 60px -24px rgb(2 42 42 / 0.4);
}
.gate::backdrop { background: rgb(2 42 42 / 0.45); }

.gate-title {
  margin: 0 0 0.6rem;
  font-family: var(--font-display);
  font-size: 1.3rem;
  line-height: 1.15;
  letter-spacing: -0.015em;
}
.gate-text {
  margin: 0 0 1.25rem;
  font-size: 0.94rem;
  line-height: 1.55;
  color: color-mix(in oklab, var(--ink) 72%, transparent);
}
.gate .gbtn { width: 100%; justify-content: center; }
.gate-note {
  margin: 0.9rem 0 0;
  font-size: 0.78rem;
  line-height: 1.45;
  color: color-mix(in oklab, var(--ink) 58%, transparent);
}
.gate-msg {
  margin: 0.9rem 0 0;
  padding: 0.55rem 0.75rem;
  border-radius: 8px;
  font-size: 0.85rem;
  background: color-mix(in oklab, var(--jj-500) 8%, transparent);
}
.gate-msg.is-bad {
  background: color-mix(in oklab, #98111F 12%, transparent);
  color: #7C0E1A;
}
.gate-msg[hidden] { display: none; }

.gate-cancel {
  display: block;
  margin: 1.1rem auto 0;
  padding: 0.4rem 0.8rem;
  border: 0;
  background: transparent;
  font: inherit;
  font-size: 0.86rem;
  color: color-mix(in oklab, var(--ink) 58%, transparent);
  text-decoration: underline;
  text-underline-offset: 2px;
  cursor: pointer;
}
.gate-cancel:hover { color: var(--ink); }

/* ── The pager ───────────────────────────────────────────────────────
   Replaces Laravel's Tailwind pagination view, which this site could
   not use: it ships <svg class="w-5 h-5"> chevrons, and with no
   Tailwind to define w-5 the SVG filled its container — one arrow
   about 500px tall under the article grid.

   Arrows here are text characters, as everywhere else on the site
   ("All articles →"). There is nothing that can escape its box.

   Laid out as one row that WRAPS, with the count pushed to its own end
   — at 390px a nine-page run plus both step links does not fit on one
   line, and a pager that scrolls sideways is worse than one that
   stacks. */
.pager {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 0.4rem 1rem;
  margin-top: clamp(2.5rem, 5vw, 3.5rem);
  padding-top: 1.5rem;
  border-top: var(--hairline-ink);
  font-size: 0.88rem;
  letter-spacing: 0.04em;
}

.pager-step {
  text-decoration: none;
  white-space: nowrap;
  font-weight: 600;
}
.pager-step:hover { text-decoration: underline; }

/* A disabled step keeps its space rather than collapsing, so the page
   numbers do not shift sideways as you move through the set. */
.pager-step.is-off {
  color: color-mix(in oklab, var(--ink) 35%, transparent);
  cursor: default;
}

.pager-list {
  display: flex;
  align-items: center;
  gap: 0.15rem;
  list-style: none;
  margin: 0;
  padding: 0;
  /* Takes the slack so the two step links sit at the outer edges. */
  margin-inline: auto;
}

.pager-n {
  display: grid;
  place-items: center;
  min-width: 2.25rem;
  min-height: 2.25rem;
  padding-inline: 0.4rem;
  border-radius: 0.25rem;
  text-decoration: none;
  font-variant-numeric: tabular-nums;
}
.pager-n:hover { background: color-mix(in oklab, var(--ink) 6%, transparent); }

.pager-n.is-here {
  background: var(--jj-500);
  color: var(--showcase);
  font-weight: 700;
}

.pager-gap {
  padding-inline: 0.35rem;
  color: color-mix(in oklab, var(--ink) 45%, transparent);
}

.pager-count {
  margin: 0;
  font-size: var(--text-eyebrow);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: color-mix(in oklab, var(--ink) 55%, transparent);
  font-variant-numeric: tabular-nums;
  /* Its own line below the controls at every width. Beside them it was
     the widest thing in the row and pushed the numbers off centre. */
  flex-basis: 100%;
  order: 1;
}

/* ── The mobile drawer ───────────────────────────────────────────────
   Below 40rem the four nav links move out of the scrolling strip and
   into a panel that hangs off the bar. The strip above stays as the
   no-JS fallback; everything here is gated on html.js, so with script
   off none of it applies and the row behaves exactly as before.

   Not a <dialog>. showModal() puts the panel in the top layer, where
   the header's own stacking no longer applies, and ::backdrop cannot
   express the closed position this animates from.

   The burger is hidden by default and only appears in the narrow tier,
   so the desktop bar is untouched. */
.burger { display: none; }

@media (min-width: 40.0625rem) {
  /* Belt and braces: even if the drawer were somehow left open, above
     the breakpoint the panel is a plain row again. */
  .navscrim { display: none !important; }
}

@media (max-width: 40rem) {
  .js .burger {
    display: grid;
    place-items: center;
    width: 2.75rem;
    height: 2.75rem;
    margin-right: -0.5rem;   /* optical, against the wrap's gutter */
    flex: none;
    padding: 0;
    border: 0;
    background: none;
    color: var(--ink);
    cursor: pointer;
  }
  .js .burger svg { width: 1.5rem; height: 1.5rem; }
  .js .burger-b {
    stroke: currentColor;
    stroke-width: 2;
    stroke-linecap: round;
    /* VIEW-BOX, NEVER FILL-BOX. A stroked horizontal line has no fill,
       so its fill box is zero-height and the rotation origin collapses.
       The list reads right to left, so each bar walks to the centre
       first and then turns about it. */
    transform-box: view-box;
    transform-origin: center;
    transition: transform .28s cubic-bezier(.4, 0, .2, 1);
  }
  .js .burger[aria-expanded="true"] .burger-b1 { transform: rotate(45deg) translateY(4px); }
  .js .burger[aria-expanded="true"] .burger-b2 { transform: rotate(-45deg) translateY(-4px); }
  .js .burger:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 2px; }

  /* THE PANEL STARTS BELOW THE BAR, and so does the scrim. The burger
     morphs into the X that closes this, so a full-viewport panel would
     sit on top of the control it turned into and the X could not be
     pressed at all.

     THE BAR IS 66px, NOT 65. .head-inner is 4.0625rem of content and
     .site-head adds a 1px bottom border on top of that. Sizing the
     offset to the content height alone laid the panel over that border
     — measured, the panel's top was 65 against a header bottom of 66,
     so the hairline ran behind it.

     `top: 100%` DOES NOT FIX THIS, which is worth writing down because
     it looks like it should. The panel is absolute inside .site-head,
     which is sticky and so is its containing block — but a percentage
     offset resolves against the containing block's PADDING box, and the
     padding box stops inside the border. Measured: top: 100% put the
     panel back at 65. Both the panel and the fixed scrim take the
     explicit number instead, and the +1px is the border. */
  .js .site-head { --head-h: calc(4.0625rem + 1px); }

  .js .site-nav {
    position: absolute;
    top: var(--head-h);
    right: 0;
    z-index: 60;

    /* SIZES TO ITS CONTENT. Pinned top and bottom, four links left a
       tall panel with most of its height empty, which reads as
       something that failed to load. */
    display: flex;
    flex-direction: column;
    gap: 0;
    width: min(17rem, 78vw);
    padding: 0.5rem 0;
    background: var(--showcase);
    border: var(--hairline-ink);
    border-top: 0;
    border-bottom-left-radius: 0.5rem;
    box-shadow: 0 18px 40px -18px color-mix(in oklab, var(--ink) 45%, transparent);

    /* The strip's rules do not belong to a column. */
    overflow: visible;
    mask-image: none;

    /* Closed. visibility, not display, so there is something to
       transition from — and so a closed panel cannot take focus.

       IT SLIDES DOWN, NOT SIDEWAYS, AND THAT IS A BUG FIX. The closed
       panel was translateX(0.75rem) off a right: 0 anchor, and
       visibility: hidden keeps an element in layout — so those 12px sat
       outside the viewport and the document measured 402px wide at 390.
       A page that scrolls sideways because of a CLOSED menu. Vertical
       travel cannot do that: above the viewport top there is nothing to
       scroll to. It also suits a panel hanging off a bar better, which
       is what this is. */
    visibility: hidden;
    opacity: 0;
    transform: translateY(-0.6rem);
    transition: transform .26s cubic-bezier(.4, 0, .2, 1),
                opacity .2s linear,
                visibility 0s linear .26s;
  }

  .js .site-nav[data-open] {
    visibility: visible;
    opacity: 1;
    transform: translateY(0);
    transition: transform .26s cubic-bezier(.4, 0, .2, 1),
                opacity .16s linear,
                visibility 0s;
  }

  .js .site-nav a {
    padding: 0.85rem 1.15rem;
    font-size: 0.98rem;
  }
  .js .site-nav a + a { border-top: var(--hairline-ink); }
  .js .site-nav a[aria-current="page"] {
    background: color-mix(in oklab, var(--jj-500) 8%, transparent);
    font-weight: 600;
  }

  .js .navscrim {
    position: fixed;
    inset: var(--head-h) 0 0 0;
    z-index: 55;
    background: color-mix(in oklab, var(--ink) 38%, transparent);
    opacity: 0;
    transition: opacity .2s linear;
  }
  .js .navscrim[data-open] { opacity: 1; }
}

/* The panel slides; with reduced motion it simply appears. */
@media (prefers-reduced-motion: reduce) {
  .js .site-nav,
  .js .navscrim,
  .js .burger-b { transition-duration: 0s !important; }
}

/* ── The lead album ──────────────────────────────────────────────────
   /albums used to be a bare .cardgrid directly under the page title,
   and it failed at the count it actually launches with. Measured at
   1440 with one album published: the grid built four tracks whatever
   the item count, so a single 310px card sat in a 1325px row with
   1015px of it empty, and the page finished at 752px against a 900px
   viewport. The card also began at exactly 0px below the h1, because
   .page-open carries no bottom padding and a grid's first child brings
   no margin of its own.

   Same lead-then-cards shape the articles index uses. The difference
   is what the lead shows: an album IS photographs, so it prints a
   strip of frames from inside rather than a cover and a number. */
.alead {
  margin-top: clamp(1.5rem, 3vw, 2.75rem);
  margin-bottom: clamp(2.5rem, 5vw, 4rem);
  display: grid;
  grid-template-columns: 1fr;
  gap: clamp(1.25rem, 2.5vw, 2.25rem);
}

@media (min-width: 60rem) {
  .alead {
    /* Wider than the articles lead's 1.15fr. The payload here is the
       picture, and the body column carries a title, a count and a
       thumbnail strip rather than a paragraph of summary. */
    grid-template-columns: 1.35fr 1fr;
    align-items: center;
  }
}

.alead-cover {
  display: block;
  border-radius: var(--radius);
  overflow: hidden;
  background: color-mix(in oklab, var(--ink) 6%, transparent);
}
/* aspect-ratio reserves the box before the bytes land. Without it the
   whole page jumps as the cover loads, which is the one realistic way
   to break the layout-shift budget. */
.alead-cover img {
  display: block;
  width: 100%;
  /* 3:2, not 4:3. At 1440 the taller box put the cover at 740px and it
     swallowed the page — the body column beside it is a title, a count
     and a thumbnail row, so a cover half again its height reads as one
     picture with some notes rather than as a lead. */
  aspect-ratio: 3 / 2;
  object-fit: cover;
}

.alead-body { min-width: 0; }

.alead-title {
  margin: 0.35rem 0 0;
  font-size: var(--text-display-m);
  line-height: 1.12;
}
.alead-title a { color: inherit; text-decoration: none; }
.alead-title a:hover { text-decoration: underline; }

.alead-desc {
  max-width: var(--measure);
  margin: 0.85rem 0 0;
  line-height: 1.6;
  color: color-mix(in oklab, var(--ink) 72%, transparent);
}

/* THE STRIP IS DECORATION, NOT NAVIGATION. Every frame links to the
   same album the title already links to, so it is aria-hidden and its
   anchors are out of the tab order — otherwise a keyboard user tabs
   through five links that all go to one place. */
.alead-strip {
  list-style: none;
  margin: 1.4rem 0 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(3.5rem, 1fr));
  gap: 0.5rem;
}
.alead-strip img {
  display: block;
  width: 100%;
  aspect-ratio: 1;
  object-fit: cover;
  border-radius: calc(var(--radius) * 0.5);
  background: color-mix(in oklab, var(--ink) 6%, transparent);
}
.alead-strip a { display: block; }

/* BIGGER TILES ON A PHONE. A 3.5rem floor put five 64px thumbnails in a
   row, which is too small to read as a photograph of anything.

   THE FLOOR IS A PERCENTAGE OF THE VIEWPORT, NOT A FIXED LENGTH, and
   both ends of the range are why. A flat 6rem gave a tidy three across
   at 360-414, only TWO at 320 (136px tiles, a 2+2+1 split), and FOUR at
   480 once the fixed value stopped being the larger of the two. Scaling
   the floor with the screen holds three across the whole range, so the
   five frames land as 3+2 from 320 to 480. Above that the default floor
   already fits all five on one row. */
@media (max-width: 30rem) {
  .alead-strip { grid-template-columns: repeat(auto-fit, minmax(26vw, 1fr)); }
}

/* "+5" — what the strip could not show, so the row reads as a sample of
   something larger rather than as the whole album. It sits ON the last
   frame, not beside it; as its own tile the grid stranded it alone on a
   second row. */
.alead-strip li { position: relative; }
.alead-more {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  border-radius: calc(var(--radius) * 0.5);
  /* Plain alpha, not color-mix with transparent — mixing toward
     transparent in oklab darkens the result as well as fading it, and
     this has to stay dark enough to carry white type. */
  background: rgba(6, 26, 26, 0.62);
  color: #fff;
  font-size: 0.95rem;
  font-weight: 700;
}

.alead-go { display: inline-block; margin-top: 1.4rem; font-weight: 600; }

/* ONE COLUMN IS NOT A GRID. With two or three albums left over, the
   auto-fill track count still built four columns and left most of the
   row empty — the same defect the lead fixes above, one level down.
   Capping the track count to the item count is not possible in CSS, so
   the cards cap their own width instead and sit left. */
.cardgrid { justify-content: start; }
.cardgrid > .post { max-width: 32rem; }

/* ── Devanagari headings need their own leading ──────────────────────
   NOT A PREFERENCE — a measurement. With the real fonts at the 1.25
   ratio the headings use:

     Devanagari (Martel)  ink 134 units in a 125-unit line box
     Latin      (Zodiak)  ink  74 units in the same 125-unit box

   The Devanagari ink OVERFLOWS by 9 units, so the two lines of a Nepali
   heading collide — a candrabindu meets the vowel sign of the line
   above. Latin has 51 units spare in the identical box. One line-height
   cannot serve both scripts.

   :lang(), NOT :root:lang(). The site is bilingual but the CONTENT is
   not translated, so both scripts appear on both language versions: a
   Nepali article title sits on the English home page, and English
   titles fill the Nepali one. Hung off the document, this rule would
   loosen those English titles on the Nepali site AND still leave the
   Nepali one cramped on the English site — wrong in both directions.
   Every content title declares its own lang (@textlang, see
   App\Support\Script), so :lang() resolves per element. Interface
   strings carry no attribute and correctly inherit the document's.

   1.45, not the 1.85 that would give Devanagari the same PROPORTIONAL
   leading Latin gets. Latin ink is small relative to its em and leaves
   a lot of room by nature; matching that ratio would set headlines like
   body copy. 1.45 clears the ink with room to spare and still reads as
   a headline.

   EVERY SELECTOR TESTS THE ELEMENT'S OWN LANGUAGE. A descendant form
   like `:lang(ne) .display` would match any title inside a Nepali
   document regardless of what the title itself declares, which is the
   exact case this is here to separate. `.display:lang(ne)` resolves the
   element's own inherited-or-declared language, so a title carrying
   lang="en" on the Nepali site does not match and keeps the ratio it
   was designed with — no counter-rule needed, and none should be added:
   `line-height: inherit` as a reset would throw away each title's own
   value rather than restore it.

   ONE THING THIS DELIBERATELY DOES NOT EXCLUDE: .plate-glyph, the
   decorative initial on a coverless card, is `.display` with no lang of
   its own, so on the Nepali site it inherits ne and its line box goes
   from 64px to 93px. Measured, that changes nothing — .plate centres
   with place-items and the glyph centres within its own line box, so
   its offset from the plate's centre is 0 either way and it never
   overflows. Checked so the next person does not have to; do not add a
   rule for it. */
.display:lang(ne),
h1:lang(ne), h2:lang(ne), h3:lang(ne) {
  line-height: 1.45;
}
