
/* ==========================================================================
   2. THE REEL SHELL
   The reading surface is untouched; only the way you move through it changed.
   Chrome is set in mono throughout — the dossier reads in serif and sans,
   the instrumentation around it reads as instrumentation.
   ========================================================================== */

:root {
  --rl-chip: color-mix(in srgb, var(--ink) 6%, transparent);
  --rl-chip-hover: color-mix(in srgb, var(--warm) 20%, transparent);
  --rl-veil: color-mix(in srgb, var(--bg) 82%, transparent);
  --rl-slideH: 100vh;
  --rl-top: 46px;
  --rl-bottom: 60px;
  /* the page box: one width, one height, one measure — for every card */
  --rl-cardw: 624px;
  --rl-cardh: 800px;
  --rc-fs: clamp(15px, .34vw + 13.9px, 16.6px);
  --rc-lh: 1.62;
  --rc-gap: 44px;
}
@supports (height: 100dvh) { :root { --rl-slideH: 100dvh; } }

html { scroll-behavior: auto; }
body {
  margin: 0;
  height: 100%;
  overflow: hidden;
  overscroll-behavior: none;
  background: var(--bg);
}
:where(button) { font: inherit; color: inherit; background: none; border: 0; cursor: pointer; }
:focus-visible { outline: 2px solid var(--warm); outline-offset: 2px; border-radius: 6px; }

/* ---------- the feed ---------------------------------------------------- */
.rl-feed {
  position: fixed; inset: 0;
  overflow-y: scroll; overflow-x: hidden;
  scroll-snap-type: y mandatory;
  overscroll-behavior-y: contain;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: none;
}
.rl-feed::-webkit-scrollbar { display: none; }

.rl-slide {
  height: var(--rl-slideH);
  scroll-snap-align: start;
  scroll-snap-stop: always;
  position: relative;
  display: flex; flex-direction: column;
  padding: calc(var(--rl-top) + env(safe-area-inset-top, 0px) + 6px)
           clamp(14px, 4vw, 30px)
           calc(var(--rl-bottom) + env(safe-area-inset-bottom, 0px) + 8px);
  box-sizing: border-box;
}
.rl-in {
  width: 100%; max-width: var(--rl-cardw); margin: 0 auto;
  flex: 1; min-height: 0;
  display: flex; flex-direction: column; justify-content: center; gap: 10px;
}
/* leave the action rail its own gutter so it never sits over a card */
@media (min-width: 761px) { .rl-slide { padding-right: 68px; } }

/* the stage — a constant frame; the cards move through it */
.rl-stage {
  flex: 1 1 auto; min-height: 0; max-height: var(--rl-cardh);
  display: flex; flex-direction: column;
}
.rl-stage > .rl-stage-in {
  flex: 1; min-width: 0; min-height: 0;
  display: flex; flex-direction: column; justify-content: center;
}

/* Each reel is standalone, so the block's own leading margin goes. Nothing
   else about its styling is touched — siblings inside an opener keep theirs. */
.rl-stage-in > :first-child { margin-top: 0 !important; }

/* ==========================================================================
   THE PAGE
   Every reel is the same object: running head, a page window, a folio. One
   face, one ground, one measure — so what changes between cards is only ever
   the writing. Nothing outgrows the window: what doesn't fit becomes pages you
   turn, which is why the box never scrolls.

   The window has a ceiling, not a fixed height. A card that needs pages fills
   it; a short card closes at the end of its text. Same object either way —
   only the paper is cut to length, the way a card should be.
   ========================================================================== */
.rc {
  flex: 1 1 auto; min-height: 0;
  display: flex; flex-direction: column;
  position: relative; overflow: hidden;
  background: var(--bg-elev);
  border: 1px solid var(--line);
  border-radius: 18px;
  box-shadow: var(--shadow);
  padding: clamp(15px, 2.4vw, 24px) clamp(17px, 3vw, 30px) clamp(11px, 1.6vw, 16px);
}
/* set by layout() on a card that fits in one page: stop growing, take the
   measured height of the text and no more. The stage gives up its claim on the
   slide at the same time, so the card and its hashtags stay one centred group
   instead of drifting to opposite ends of the screen. */
.rc[data-hug="1"], .rc[data-hug="1"] > .rc-body { flex: 0 0 auto; }
.rl-slide[data-hug="1"] .rl-stage { flex: 0 1 auto; }
/* the gauge: present only on a card that has pages, filling as you turn them.
   A hairline that means something rather than a hairline that decorates. */
.rc::after {
  content: ""; position: absolute; left: 0; top: 0; height: 2px;
  width: calc(var(--rc-fill, 0) * 100%);
  background: linear-gradient(90deg, var(--warm), var(--cool));
  transition: width .3s cubic-bezier(.3, .72, .25, 1);
}
.rc[data-pages="1"]::after { display: none; }

/* running head — where this page sits in the dossier */
.rc-run {
  flex: 0 0 auto; display: flex; align-items: baseline; gap: 8px;
  font-family: var(--mono); font-size: .6rem; letter-spacing: .13em;
  text-transform: uppercase; color: var(--ink-faint);
  padding-bottom: clamp(9px, 1.3vw, 13px);
  border-bottom: 1px solid var(--line);
  margin-bottom: clamp(11px, 1.7vw, 17px);
}
.rc-runl { display: flex; align-items: baseline; gap: 8px; min-width: 0; flex: 0 1 auto; }
.rc-run .rc-pt { color: var(--cool-ink); flex: 0 0 auto; }
.rc-run .rc-sec {
  color: var(--warm-ink); flex: 0 1 auto;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.rc-run .rc-run2 {
  flex: 1 1 auto; min-width: 0; padding-left: 9px;
  border-left: 1px solid var(--line);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.rc-run .rc-of {
  margin-left: auto; padding-left: 10px; flex: 0 0 auto;
  font-variant-numeric: tabular-nums; white-space: nowrap;
}

/* head — one display size, whatever the source element was */
.rc-h { flex: 0 0 auto; }
.rc-h:empty { display: none; }
.rc .rc-kick {
  font-family: var(--mono); font-size: .63rem; letter-spacing: .13em;
  text-transform: uppercase; color: var(--warm-ink);
  margin: 0 0 .5em; display: block; line-height: 1.5;
}
.rc .rc-kick > * { margin: 0; }
.rc .rc-head {
  font-family: var(--serif); font-weight: 600; letter-spacing: -.012em;
  font-size: clamp(1.15rem, 1.5vw + .55rem, 1.46rem); line-height: 1.22;
  text-wrap: balance; color: var(--ink);
  margin: 0 0 clamp(10px, 1.5vw, 15px);
}

/* A title with nothing under it is a part-title plate, not a page missing its
   body. Same skin, same faces — the title just gets the room a book gives a
   part title, rather than being stranded at the top of an empty box. */
.rc[data-plate="1"] .rc-body { display: none; }
.rc[data-plate="1"] .rc-h {
  margin: auto 0; text-align: center; padding: clamp(34px, 7vw, 76px) 0;
}
.rc[data-plate="1"] .rc-kick { margin-bottom: 1.1em; }
.rc[data-plate="1"] .rc-kick::after {
  content: ""; display: block; width: 26px; height: 1px;
  margin: .85em auto 0; background: var(--line-strong);
}
.rc[data-plate="1"] .rc-head {
  font-size: clamp(1.42rem, 2.1vw + .7rem, 2.05rem);
  line-height: 1.16; margin: 0; max-width: 22ch; margin-inline: auto;
}
.rc[data-plate="1"] .rc-f { margin-top: 0; }

/* the page window — the flow inside it is set in columns one window wide, so
   turning a page slides sideways by exactly one window. Lines are never cut. */
.rc-body {
  flex: 1 1 auto; min-height: 0; overflow: hidden; position: relative;
  scrollbar-width: thin; scrollbar-color: var(--line-strong) transparent;
}
.rc-body::-webkit-scrollbar { width: 5px; }
.rc-body::-webkit-scrollbar-thumb { background: var(--line-strong); border-radius: 3px; }
.rc-flow {
  height: 100%; position: relative;
  column-width: var(--rc-colw, 560px);
  column-gap: var(--rc-gap);
  column-fill: auto;
  transform: translateX(calc(-1 * var(--rc-page, 0) * (var(--rc-colw, 560px) + var(--rc-gap))));
  transition: transform .34s cubic-bezier(.3, .72, .25, 1);
}
/* while a card is being photographed its pages are turned one after another to
   be measured. Animating that would mean measuring a card mid-slide. */
.rc[data-still="1"] .rc-flow, .rc[data-still="1"]::after { transition: none; }

/* one measure for running text, everywhere */
.rc .rc-flow {
  font-family: var(--sans); font-size: var(--rc-fs); line-height: var(--rc-lh);
  color: var(--ink-soft);
}
.rc .rc-flow > :first-child { margin-top: 0 !important; }
.rc .rc-flow p, .rc .rc-flow li { font-size: 1em; line-height: var(--rc-lh); orphans: 2; widows: 2; }
.rc .rc-flow p { margin: 0 0 .92em; }
.rc .rc-flow h3, .rc .rc-flow h4 {
  font-family: var(--serif); font-weight: 600; color: var(--ink);
  font-size: 1.05em; line-height: 1.3; letter-spacing: -.008em;
  margin: 1.25em 0 .45em;
}
/* the dossier's three display voices keep their family and colour but come
   back to the page measure, so a quote can never eat a whole page */
.rc .rc-flow .lede, .rc .rc-flow .q, .rc .rc-flow .callout .q, .rc .rc-flow blockquote {
  font-family: var(--serif); font-size: 1.11em; line-height: 1.42;
  font-weight: 600; color: var(--ink);
}
.rc .rc-flow strong { color: var(--ink); }
.rc .rc-flow ul, .rc .rc-flow ol { margin: 0 0 .92em; padding-left: 1.15em; }
.rc .rc-flow li { margin-bottom: .35em; }
/* A page 570px wide cannot show three columns side by side, and — the reason
   this matters — a grid container cannot be split across a page turn: the
   browser overflows it instead, and everything past the fold would be lost.
   Stacking the panel grids answers both problems with one rule. */
/* `overflow: hidden` also has to go: a scroll or clip container is monolithic
   to the fragmenter, which is the other half of why these were being cut. */
.rc .rc-flow :is(.bio, .commit, .loop, .deck-grid, .metrics, .toc-flow,
  .cardtypes, .wcw, .arms, .stat-grid, .tiers, .endpoints) {
  display: block; background: transparent; padding: 0; gap: 0; border: 0;
  overflow: visible; border-radius: 0;
}
.rc .rc-flow :is(.bio, .commit, .loop, .deck-grid, .metrics, .toc-flow,
  .cardtypes, .wcw, .arms, .stat-grid, .tiers, .endpoints) > * {
  break-inside: avoid;              /* a panel and its label turn together */
}
.rc .rc-flow :is(.bio, .commit, .loop, .deck-grid, .metrics, .toc-flow,
  .cardtypes, .wcw, .arms, .stat-grid, .tiers, .endpoints) > * + * {
  margin-top: 12px; padding-top: 12px; border-top: 1px solid var(--line);
}
/* the version timeline is a label beside a body: stack it, but plainly */
.rc .rc-flow .ver { display: block; gap: 0; }
.rc .rc-flow .ver > * + * { margin-top: 10px; }

/* A kill criterion is a ✕ and then a sentence — but a flex row makes an item of
   every child, so each `em` and `b` inside the sentence became its own column and
   the prose came apart into ribbons. The mark hangs in the margin instead, which
   is what it looked like it was doing all along. */
.rc .rc-flow .kill .rule { display: block; position: relative; padding-left: 1.5em; }
.rc .rc-flow .kill .rule::before { position: absolute; left: 0; top: 0; }

/* tables become stacked rows inside a page, always — the same treatment they
   already had on narrow screens. Each row is then a block that can move to the
   next page on its own, which a table cell never could. */
.rc .rc-flow .table-scroll { border: 0; box-shadow: none; overflow: visible; padding: 0; background: transparent; }
.rc .rc-flow table { display: block; background: transparent; border: 0; min-width: 0; }
.rc .rc-flow thead { display: none; }
.rc .rc-flow tbody, .rc .rc-flow tr { display: block; width: 100%; min-width: 0; }
.rc .rc-flow tr {
  background: transparent; border: 1px solid var(--line);
  border-radius: 12px; margin-bottom: 9px; padding: 3px 2px;
}
/* a blockified cell still gets handed its old table-column width, which would
   leave four narrow ribbons stacked on top of each other — take it back */
.rc .rc-flow td { display: block; border-bottom: 0; padding: .26rem .78rem;
  width: auto !important; max-width: none; line-height: 1.52; }
.rc .rc-flow td::before {
  content: attr(data-label);
  display: block; font-family: var(--mono); font-size: .58rem;
  letter-spacing: .13em; text-transform: uppercase;
  color: var(--ink-faint); margin-bottom: .1rem;
}
.rc .rc-flow td:empty, .rc .rc-flow td:empty::before { display: none; }

/* nothing may be split across a page turn if it can be helped */
.rc .rc-flow :is(h3, h4, .rc-kick, .eyebrow) { break-after: avoid; }
.rc .rc-flow :is(tr, .stat, .arm, .ep, .kill, .toc-col, .mode, .sig, .step,
  .ctype, .mcol, .tier, .deck, .rung, .lstep, .assume, .diagram, .ver-mark,
  .grade-legend, figure, blockquote, li) { break-inside: avoid; }

/* folio — sources on the left, page turner on the right, constant height so
   the page geometry is identical whether or not a card cites anything */
.rc-f {
  flex: 0 0 auto; display: flex; align-items: center; gap: 10px;
  min-height: 30px;
  margin-top: clamp(9px, 1.5vw, 15px);
  /* the dossier's own `footer` rule carries 4rem of bottom padding — reset the
     whole box, not just the edge we want */
  padding: clamp(7px, 1.1vw, 11px) 0 0;
  border-top: 1px solid var(--line);
  font-family: var(--mono); font-size: .62rem; letter-spacing: .04em;
  color: var(--ink-faint);
}
.rc-srcs { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; gap: 2px; line-height: 1.45; }
.rc-srcs > * { margin: 0; }
.rc-srcs .rc-src, .rc-srcs .src { margin: 0; font-size: 1em; color: var(--ink-faint); }
.rc-srcs a { color: var(--cool-ink); }
.rc-fol { flex: 0 0 auto; margin-left: auto; display: flex; align-items: center; gap: 1px; }
.rc[data-pages="1"] .rc-fol { display: none; }
.rc-nav {
  width: 30px; height: 30px; border-radius: 9px; display: grid; place-items: center;
  color: var(--ink-faint); border: 1px solid transparent;
  transition: color .15s, border-color .15s;
}
.rc-nav:hover:not([disabled]) { color: var(--warm-ink); border-color: color-mix(in srgb, var(--warm) 40%, transparent); }
.rc-nav[disabled] { opacity: .26; cursor: default; }
.rc-nav svg { width: 14px; height: 14px; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
.rc-pg {
  min-width: 46px; text-align: center; color: var(--ink-soft);
  font-variant-numeric: tabular-nums; letter-spacing: .09em;
}

/* --- two fixes carried over from the last review of the dossier ---------- */
/* 1. Four labels are coloured by an inline style with no dark-mode variant,
      so they fall below contrast on the dark ground. Lift them there only. */
@media (prefers-color-scheme: dark) {
  [style*="9A63D0"] { color: #C79BEC !important; }
  [style*="C6564D"] { color: #F58B82 !important; }
}
:root[data-theme="dark"] [style*="9A63D0"] { color: #C79BEC !important; }
:root[data-theme="dark"] [style*="C6564D"] { color: #F58B82 !important; }
:root[data-theme="light"] [style*="9A63D0"] { color: #9A63D0 !important; }
:root[data-theme="light"] [style*="C6564D"] { color: #C6564D !important; }
/* 2. Safari ignores var() inside an SVG presentation attribute, so the two
      signature waves lost their stroke. Declare the strokes in CSS instead. */
.wave path:nth-of-type(1) { stroke: var(--warm); }
.wave path:nth-of-type(2) { stroke: var(--cool); }

/* cover + chapter reels are dividers, not pages: they go full-bleed */
.rl-slide[data-kind="cover"], .rl-slide[data-kind="chapter"] { padding-left: 0; padding-right: 0; }
.rl-slide[data-kind="cover"] .rl-in,
.rl-slide[data-kind="chapter"] .rl-in { max-width: 1080px; }
.rl-slide[data-kind="cover"] .rl-stage,
.rl-slide[data-kind="chapter"] .rl-stage,
.rl-slide[data-kind="end"] .rl-stage { max-height: 100%; overflow-y: auto; }
.rl-slide[data-kind="cover"] .hero { border-radius: 16px; margin: 0 clamp(10px, 3vw, 22px); }
/* the cover and the chapter plates are not rateable content, so the action
   rail stands down rather than sitting on top of them */
body[data-plate="1"] .rl-acts { visibility: hidden; pointer-events: none; }
.rl-slide[data-kind="cover"] .hero-inner { padding: clamp(1.6rem, 4vw, 3rem) clamp(1.2rem, 4vw, 2.6rem); }
.rl-slide[data-kind="chapter"] .part-band { border: 0; }

/* ---------- hashtags --------------------------------------------------- */
.rl-tags { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; }
.rl-tag {
  font-family: var(--mono); font-size: .69rem; letter-spacing: .01em;
  padding: 4px 9px; border-radius: 999px;
  background: var(--rl-chip); color: var(--ink-soft);
  border: 1px solid transparent; white-space: nowrap;
  transition: background .15s, color .15s, border-color .15s;
}
.rl-tag:hover, .rl-tag:focus-visible { background: var(--rl-chip-hover); color: var(--warm-ink); border-color: color-mix(in srgb, var(--warm) 45%, transparent); }
.rl-tag[data-hot="1"] { color: var(--warm-ink); border-color: color-mix(in srgb, var(--warm) 38%, transparent); background: color-mix(in srgb, var(--warm) 11%, transparent); }
.rl-tag[data-muted="1"] { opacity: .38; text-decoration: line-through; }
.rl-why {
  font-family: var(--mono); font-size: .64rem; color: var(--ink-faint);
  padding: 4px 8px; border-radius: 999px; border: 1px dashed var(--line-strong);
}
.rl-why:hover { color: var(--cool-ink); border-color: var(--cool); }
/* the address, at the end of the row the hashtags sit in. It is here for the
   same reason it is in the exported picture: a card that travels should say
   where it came from. */
.rl-site {
  margin-left: auto; flex: 0 0 auto; padding: 4px 1px;
  font-family: var(--mono); font-size: .62rem; letter-spacing: .09em;
  color: var(--ink-faint); text-decoration: none; white-space: nowrap;
  transition: color .15s;
}
.rl-site:hover, .rl-site:focus-visible { color: var(--warm-ink); }

/* ---------- top bar ---------------------------------------------------- */
.rl-top {
  position: fixed; top: 0; left: 0; right: 0; z-index: 40;
  height: calc(var(--rl-top) + env(safe-area-inset-top, 0px));
  padding: env(safe-area-inset-top, 0px) clamp(10px, 3vw, 18px) 0 clamp(16px, 3vw, 24px);
  display: flex; align-items: center; gap: 10px;
  background: var(--rl-veil);
  backdrop-filter: saturate(140%) blur(14px);
  -webkit-backdrop-filter: saturate(140%) blur(14px);
  border-bottom: 1px solid var(--line);
}
.rl-brand {
  display: flex; align-items: center; gap: 7px;
  font-family: var(--mono); font-size: .74rem; letter-spacing: .16em;
  text-transform: uppercase; color: var(--ink); flex-shrink: 0;
}
.rl-brand .rl-dot {
  width: 8px; height: 8px; border-radius: 50%;
  background: var(--warm); box-shadow: 0 0 12px var(--warm);
}
.rl-scope {
  font-family: var(--mono); font-size: .68rem; color: var(--ink-soft);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1;
}
.rl-scope b { color: var(--warm-ink); font-weight: 600; }
.rl-count {
  font-family: var(--mono); font-size: .68rem; color: var(--ink-faint);
  font-variant-numeric: tabular-nums; flex-shrink: 0;
}
.rl-iconbtn {
  width: 30px; height: 30px; border-radius: 8px; flex-shrink: 0;
  display: grid; place-items: center; color: var(--ink-soft);
  transition: background .15s, color .15s;
}
.rl-iconbtn:hover { background: var(--rl-chip); color: var(--ink); }
.rl-iconbtn svg { width: 16px; height: 16px; fill: none; stroke: currentColor; stroke-width: 1.6; stroke-linecap: round; stroke-linejoin: round; }

/* ---------- part progress rail ----------------------------------------- */
.rl-rail {
  position: fixed; left: 0; z-index: 35;
  top: calc(var(--rl-top) + env(safe-area-inset-top, 0px) + 10px);
  bottom: calc(var(--rl-bottom) + env(safe-area-inset-bottom, 0px) + 10px);
  width: 3px; display: flex; flex-direction: column; gap: 3px;
}
.rl-rail i {
  flex: 1; position: relative; border-radius: 3px;
  background: var(--line); overflow: hidden;
}
.rl-rail i::after {
  content: ''; position: absolute; left: 0; right: 0; top: 0;
  height: var(--f, 0%);
  background: linear-gradient(180deg, var(--warm), var(--cool));
  transition: height .4s ease;
}
.rl-rail i[data-here="1"] { background: color-mix(in srgb, var(--warm) 28%, var(--line)); box-shadow: 0 0 8px var(--glow-warm); }

/* ---------- action rail ------------------------------------------------ */
.rl-acts {
  position: fixed; right: clamp(8px, 2vw, 16px); top: 50%;
  transform: translateY(-50%); z-index: 35;
  display: flex; flex-direction: column; gap: 8px;
}
.rl-act {
  width: 40px; height: 40px; border-radius: 12px;
  display: grid; place-items: center; position: relative;
  color: var(--ink-soft); background: var(--rl-veil);
  border: 1px solid var(--line);
  backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px);
  transition: color .15s, border-color .15s, transform .15s, background .15s;
}
.rl-act:hover { color: var(--ink); transform: translateY(-1px); }
.rl-act svg { width: 18px; height: 18px; fill: none; stroke: currentColor; stroke-width: 1.7; stroke-linecap: round; stroke-linejoin: round; }
.rl-act[data-on="1"] { color: var(--warm-ink); border-color: color-mix(in srgb, var(--warm) 50%, transparent); background: color-mix(in srgb, var(--warm) 13%, transparent); }
.rl-act[data-on="1"] svg { fill: color-mix(in srgb, var(--warm) 30%, transparent); }
.rl-act[data-kind="nope"][data-on="1"] { color: var(--crit); border-color: color-mix(in srgb, var(--crit) 50%, transparent); background: color-mix(in srgb, var(--crit) 12%, transparent); }
.rl-act[data-kind="nope"][data-on="1"] svg { fill: none; }
.rl-act .rl-lab {
  position: absolute; right: 46px; top: 50%; transform: translateY(-50%);
  font-family: var(--mono); font-size: .62rem; letter-spacing: .1em;
  text-transform: uppercase; color: var(--ink-faint);
  background: var(--bg-elev); border: 1px solid var(--line);
  padding: 3px 7px; border-radius: 6px; white-space: nowrap;
  opacity: 0; pointer-events: none; transition: opacity .15s;
}
.rl-act:hover .rl-lab { opacity: 1; }
@media (max-width: 760px) {
  .rl-acts { top: auto; transform: none; bottom: calc(var(--rl-bottom) + env(safe-area-inset-bottom, 0px) + 12px); }
  .rl-act { width: 36px; height: 36px; }
  .rl-act .rl-lab { display: none; }
}

/* ---------- bottom bar ------------------------------------------------- */
.rl-bar {
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 40;
  min-height: var(--rl-bottom);
  padding: 8px clamp(10px, 3vw, 18px) calc(8px + env(safe-area-inset-bottom, 0px));
  display: flex; align-items: center; justify-content: center; gap: 6px;
  background: var(--rl-veil);
  backdrop-filter: saturate(140%) blur(14px);
  -webkit-backdrop-filter: saturate(140%) blur(14px);
  border-top: 1px solid var(--line);
}
.rl-modes { display: flex; gap: 2px; padding: 2px; border-radius: 10px; background: var(--rl-chip); }
.rl-mode {
  font-family: var(--mono); font-size: .66rem; letter-spacing: .06em;
  text-transform: uppercase; padding: 6px 10px; border-radius: 8px;
  color: var(--ink-faint); transition: background .15s, color .15s;
}
.rl-mode:hover { color: var(--ink); }
.rl-mode[aria-pressed="true"] { background: var(--bg-elev); color: var(--warm-ink); box-shadow: var(--shadow); }
.rl-barbtn {
  font-family: var(--mono); font-size: .66rem; letter-spacing: .06em;
  text-transform: uppercase; padding: 7px 11px; border-radius: 9px;
  color: var(--ink-soft); border: 1px solid var(--line);
  display: inline-flex; align-items: center; gap: 6px;
  transition: background .15s, color .15s, border-color .15s;
}
.rl-barbtn:hover { background: var(--rl-chip); color: var(--ink); border-color: var(--line-strong); }
.rl-barbtn svg { width: 13px; height: 13px; fill: none; stroke: currentColor; stroke-width: 1.7; stroke-linecap: round; stroke-linejoin: round; }
@media (max-width: 620px) { .rl-barbtn .rl-btxt { display: none; } .rl-barbtn { padding: 8px; } }

/* ---------- overlays --------------------------------------------------- */
.rl-ov {
  position: fixed; inset: 0; z-index: 60;
  background: color-mix(in srgb, var(--bg) 88%, transparent);
  backdrop-filter: blur(18px); -webkit-backdrop-filter: blur(18px);
  display: none; flex-direction: column;
}
.rl-ov[data-open="1"] { display: flex; }
.rl-ovhead {
  display: flex; align-items: center; gap: 12px; flex-shrink: 0;
  padding: calc(14px + env(safe-area-inset-top, 0px)) clamp(14px, 4vw, 26px) 12px;
  border-bottom: 1px solid var(--line);
}
.rl-ovtitle {
  font-family: var(--mono); font-size: .74rem; letter-spacing: .16em;
  text-transform: uppercase; color: var(--ink); flex: 1;
}
.rl-ovbody {
  flex: 1; overflow-y: auto;
  padding: clamp(16px, 4vw, 26px) clamp(14px, 4vw, 26px) calc(40px + env(safe-area-inset-bottom, 0px));
}
.rl-ovbody-in { max-width: 780px; margin: 0 auto; }
.rl-ovnote {
  font-size: .88rem; line-height: 1.6; color: var(--ink-soft);
  max-width: 62ch; margin: 0 0 1.4rem;
}
.rl-ovnote b { color: var(--ink); }

/* keyboard legend */
.rl-keys {
  display: grid; grid-template-columns: max-content 1fr;
  gap: 7px 16px; margin: 0 0 1.2rem; align-items: baseline;
}
.rl-keys dt {
  font-family: var(--mono); font-size: .72rem; letter-spacing: .06em;
  color: var(--warm-ink); white-space: nowrap;
}
.rl-keys dd { margin: 0; font-size: .85rem; color: var(--ink-soft); }

/* index */
.rl-ixpart { margin-bottom: 1.7rem; }
.rl-ixhead {
  font-family: var(--mono); font-size: .64rem; letter-spacing: .15em;
  text-transform: uppercase; color: var(--cool-ink);
  padding-bottom: 6px; border-bottom: 1px solid var(--line); margin-bottom: 10px;
  display: flex; justify-content: space-between; gap: 10px;
}
.rl-ixhead span:last-child { color: var(--ink-faint); font-variant-numeric: tabular-nums; }
.rl-ixname { font-family: var(--serif); font-size: 1.02rem; color: var(--ink); text-transform: none; letter-spacing: 0; }
.rl-ixgrid { display: grid; grid-template-columns: repeat(auto-fill, minmax(190px, 1fr)); gap: 6px; }
.rl-ixitem {
  display: flex; align-items: center; gap: 9px; text-align: left;
  padding: 9px 11px; border-radius: 10px; border: 1px solid var(--line);
  background: var(--bg-elev); transition: border-color .15s, transform .15s;
}
.rl-ixitem:hover { border-color: var(--warm); transform: translateY(-1px); }
.rl-ixitem .rl-ixlab { flex: 1; font-size: .92rem; color: var(--ink); }
.rl-ixitem .rl-ixn { font-family: var(--mono); font-size: .64rem; color: var(--ink-faint); font-variant-numeric: tabular-nums; }
.rl-ixitem .rl-ixbar { width: 26px; height: 3px; border-radius: 2px; background: var(--line); overflow: hidden; flex-shrink: 0; }
.rl-ixitem .rl-ixbar i { display: block; height: 100%; background: var(--warm); }

/* interests */
.rl-heat { display: flex; flex-direction: column; gap: 5px; margin-bottom: 1.8rem; }
.rl-hrow { display: flex; align-items: center; gap: 10px; }
.rl-hrow .rl-hname {
  font-family: var(--mono); font-size: .72rem; width: 10.5rem; flex-shrink: 0;
  color: var(--ink); overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.rl-hrow[data-muted="1"] .rl-hname { opacity: .4; text-decoration: line-through; }
.rl-htrack { flex: 1; height: 8px; border-radius: 4px; background: var(--rl-chip); overflow: hidden; }
.rl-htrack i { display: block; height: 100%; border-radius: 4px; background: linear-gradient(90deg, var(--warm), var(--cool)); transition: width .4s ease; }
.rl-hrow .rl-hval { font-family: var(--mono); font-size: .66rem; color: var(--ink-faint); width: 3.1rem; text-align: right; font-variant-numeric: tabular-nums; }
.rl-hmute { font-family: var(--mono); font-size: .6rem; letter-spacing: .08em; text-transform: uppercase; color: var(--ink-faint); padding: 3px 7px; border-radius: 6px; border: 1px solid var(--line); }
.rl-hmute:hover { color: var(--crit); border-color: var(--crit); }
.rl-stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(130px, 1fr)); gap: 10px; margin-bottom: 1.6rem; }
.rl-stat { padding: 12px 14px; border-radius: 12px; border: 1px solid var(--line); background: var(--bg-elev); }
.rl-stat .rl-sn { font-family: var(--mono); font-size: 1.5rem; color: var(--warm-ink); font-variant-numeric: tabular-nums; line-height: 1.1; }
.rl-stat .rl-sl { font-family: var(--mono); font-size: .6rem; letter-spacing: .12em; text-transform: uppercase; color: var(--ink-faint); margin-top: 4px; }
.rl-danger { font-family: var(--mono); font-size: .66rem; letter-spacing: .08em; text-transform: uppercase; padding: 8px 13px; border-radius: 9px; border: 1px solid var(--crit); color: var(--crit); }
.rl-danger:hover { background: color-mix(in srgb, var(--crit) 12%, transparent); }

/* tag directory */
.rl-tagdir { display: flex; flex-wrap: wrap; gap: 7px; margin-bottom: 1.8rem; }
.rl-tagdir .rl-tag { font-size: .74rem; padding: 6px 11px; }
.rl-tagdir .rl-tag em { font-style: normal; color: var(--ink-faint); font-size: .9em; margin-left: 5px; }

/* search */
.rl-searchbox {
  width: 100%; box-sizing: border-box; padding: 12px 14px; border-radius: 12px;
  background: var(--bg-elev); border: 1px solid var(--line-strong);
  color: var(--ink); font-family: var(--sans); font-size: 1rem; margin-bottom: 1.2rem;
}
.rl-searchbox::placeholder { color: var(--ink-faint); }
.rl-hit {
  display: block; width: 100%; text-align: left; padding: 12px 14px;
  border-radius: 11px; border: 1px solid var(--line); background: var(--bg-elev);
  margin-bottom: 7px; transition: border-color .15s;
}
.rl-hit:hover { border-color: var(--warm); }
.rl-hit .rl-hmeta { font-family: var(--mono); font-size: .6rem; letter-spacing: .12em; text-transform: uppercase; color: var(--cool-ink); margin-bottom: 4px; }
.rl-hit .rl-htitle { font-family: var(--serif); font-size: 1rem; color: var(--ink); margin-bottom: 3px; }
.rl-hit .rl-hsnip { font-size: .84rem; color: var(--ink-soft); line-height: 1.5; }
.rl-hit mark { background: color-mix(in srgb, var(--warm) 32%, transparent); color: var(--ink); border-radius: 3px; padding: 0 2px; }

/* sources overlay keeps the original footer styling */
.rl-ovbody footer { border-top: 0; padding: 0; }

/* the exported pictures */
.rl-shots { display: grid; gap: 20px; grid-template-columns: repeat(auto-fill, minmax(232px, 1fr)); }
.rl-shot { margin: 0; }
.rl-shot img {
  display: block; width: 100%; height: auto;
  border-radius: 13px; border: 1px solid var(--line); background: var(--bg-elev);
}
.rl-shotb { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; margin-top: 9px; }
.rl-shotn {
  font-family: var(--mono); font-size: .58rem; letter-spacing: .12em;
  text-transform: uppercase; color: var(--ink-faint); margin-left: auto;
}

/* ---------- end card, coach mark, toast -------------------------------- */
.rl-end { text-align: center; padding: 0 1rem; }
.rl-end h3 { font-family: var(--serif); font-size: clamp(1.5rem, 4vw, 2.1rem); color: var(--ink); margin: 0 0 .7rem; text-wrap: balance; }
.rl-end p { color: var(--ink-soft); max-width: 46ch; margin: 0 auto 1.4rem; line-height: 1.6; }
.rl-endbtns { display: flex; gap: 8px; justify-content: center; flex-wrap: wrap; }

.rl-coach {
  position: fixed; left: 50%; transform: translateX(-50%); z-index: 50;
  bottom: calc(var(--rl-bottom) + env(safe-area-inset-bottom, 0px) + 6px);
  pointer-events: none;
  display: flex; align-items: center; gap: 8px;
  font-family: var(--mono); font-size: .63rem; letter-spacing: .12em;
  text-transform: uppercase; color: var(--ink-faint);
  padding: 6px 13px; border-radius: 999px;
  background: var(--rl-veil); border: 1px solid var(--line);
  backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px);
}
.rl-coach svg { width: 15px; height: 15px; fill: none; stroke: var(--warm); stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round; animation: rl-nudge 1.9s ease-in-out infinite; }
@keyframes rl-nudge { 0%, 100% { transform: translateY(3px); opacity: .55; } 50% { transform: translateY(-3px); opacity: 1; } }
/* the page-turn hint reads sideways, because that is the gesture it teaches.
   It also sits a row higher, clear of the hashtags under the card. */
.rl-coach[data-axis="x"] {
  text-transform: none; letter-spacing: .04em; max-width: min(92vw, 460px);
  bottom: calc(var(--rl-bottom) + env(safe-area-inset-bottom, 0px) + 52px);
}
.rl-coach[data-axis="x"] svg:first-child { animation: rl-nudge-l 1.9s ease-in-out infinite; }
.rl-coach[data-axis="x"] svg:last-child { animation: rl-nudge-r 1.9s ease-in-out infinite; }
@keyframes rl-nudge-l { 0%, 100% { transform: translateX(2px); opacity: .5; } 50% { transform: translateX(-2px); opacity: 1; } }
@keyframes rl-nudge-r { 0%, 100% { transform: translateX(-2px); opacity: .5; } 50% { transform: translateX(2px); opacity: 1; } }

.rl-toast {
  position: fixed; left: 50%; transform: translateX(-50%) translateY(10px); z-index: 70;
  bottom: calc(var(--rl-bottom) + env(safe-area-inset-bottom, 0px) + 20px);
  font-family: var(--mono); font-size: .68rem; letter-spacing: .06em;
  padding: 9px 15px; border-radius: 10px;
  background: var(--ink); color: var(--bg);
  opacity: 0; pointer-events: none; transition: opacity .2s, transform .2s;
}
.rl-toast[data-show="1"] { opacity: 1; transform: translateX(-50%) translateY(0); }

.rl-sr {
  position: absolute; width: 1px; height: 1px; overflow: hidden;
  clip: rect(0 0 0 0); white-space: nowrap;
}

/* entry motion */
.rl-slide[data-live="1"] .rl-in { animation: rl-rise .42s cubic-bezier(.2, .7, .3, 1) both; }
@keyframes rl-rise { from { opacity: 0; transform: translateY(11px); } to { opacity: 1; transform: none; } }
@media (prefers-reduced-motion: reduce) {
  .rl-slide[data-live="1"] .rl-in { animation: none; }
  .rl-coach svg { animation: none; }
  .rl-rail i::after, .rl-htrack i { transition: none; }
}

/* ==========================================================================
   THE DOCUMENT UNDERNEATH
   On the web the cards ship as real elements, so every word of the dossier is
   in the page before a line of this script runs. The feed is the default view
   and reads its cards out of that source; with no scripting the source shows
   instead, and its pages unbind into one continuous read.
   ========================================================================== */
#reels-src { display: none; }

/* A page window opens out to its full length wherever the reader has no way to
   turn pages — the plain document, and paper. */
.rl-flat .rc { box-shadow: none; overflow: visible; }
.rl-flat .rc::after, .rl-flat .rc-fol { display: none; }
.rl-flat .rc-body { overflow: visible; height: auto; }
.rl-flat .rc-flow {
  height: auto; column-width: auto; columns: auto; transform: none;
}

/* printing unbinds the book: every page window opens out to its full length */
@media print {
  .rl-top, .rl-bar, .rl-acts, .rl-rail, .rl-coach, .rl-toast, .rl-ov,
  .rc-fol { display: none !important; }
  body, .rl-feed { position: static; overflow: visible; height: auto; }
  .rl-slide { height: auto; page-break-after: always; }
  .rl-stage { overflow: visible; max-height: none; }
  .rc { box-shadow: none; overflow: visible; }
  .rc::after { display: none; }
  .rc-body { overflow: visible; }
  .rc-flow { height: auto; column-width: auto; columns: auto; transform: none; }
}

/* ==========================================================================
   WHERE NEXT
   Two named ways on at the foot of every card. Styled as an invitation rather
   than a control: no fill at rest, the kicker in the reading face so it looks
   like something to read, and the section it lands in underneath it in mono so
   the reader knows where they are being taken before they go.
   ========================================================================== */
.rl-next { flex: 0 0 auto; display: flex; align-items: baseline; gap: 10px;
           min-width: 0; }
.rl-next-l { flex: 0 0 auto; font-family: var(--mono); font-size: .58rem;
             letter-spacing: .16em; text-transform: uppercase;
             color: var(--ink-faint); opacity: .8; padding-top: 2px; }
.rl-next-r { flex: 1 1 auto; min-width: 0; display: grid; gap: 6px;
             grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); }
.rl-go { display: block; width: 100%; min-width: 0; text-align: left;
         padding: 6px 10px 5px; border-radius: 9px;
         border: 1px solid var(--line-strong); background: transparent;
         cursor: pointer; transition: border-color .16s, background .16s; }
/* Two lines, not one. The kickers in this ledger are whole sentences — "414
   societies, scored one variable at a time" — and a single nowrap line cut most
   of them mid-word, which defeats the entire purpose of the row: the reader is
   supposed to be able to choose a direction by reading the label. Clamped at two
   so a long kicker cannot push the card's own text off the page, and given the
   height of two either way so the row does not jump as you move between cards. */
.rl-go-k { display: -webkit-box; -webkit-box-orient: vertical;
           -webkit-line-clamp: 2; overflow: hidden;
           font-size: .74rem; line-height: 1.3; min-height: 1.92rem;
           color: var(--ink-soft); }
.rl-go-s { display: block; font-family: var(--mono); font-size: .55rem;
           letter-spacing: .12em; text-transform: uppercase;
           color: var(--ink-faint); margin-top: 3px; opacity: .75; }
.rl-go:hover { border-color: var(--warm); background: var(--rl-chip); }
.rl-go:hover .rl-go-k { color: var(--ink); }
.rl-go:focus-visible { outline: 2px solid var(--warm); outline-offset: 2px; }

/* Narrow phones: one invitation per line, and the label sits above them. The
   action rail is a fixed 36px column down the right edge, and a full-width
   button would run under it, so the row keeps clear of it the way the tag row
   below already does. */
@media (max-width: 700px) {
  .rl-next { flex-direction: column; gap: 5px; align-items: stretch;
             padding-right: 52px; }
  .rl-next-r { grid-template-columns: 1fr; }
}

/* the page-turn coach mark lands in this band; see NEXT_PATCHES */
body.rl-coaching .rl-next { visibility: hidden; }

/* the static section pages are a document, not a feed: no onward buttons there,
   because the next card is already the next thing down the page */
.rl-flat .rl-next { display: none; }

/* ==========================================================================
   ZOOM AND FONT SIZE
   The page box, in the same unit as the words in it. 2.875rem is 46px at the
   default root size, 39rem is 624px, and so on down the block — so this is a
   no-op until a reader raises their font size, and then the whole geometry
   grows with the type instead of holding still while the words swell inside it.
   ========================================================================== */
:root {
  --rl-top: 2.875rem;
  --rl-bottom: 3.75rem;
  --rl-cardw: 39rem;
  --rl-cardh: 50rem;
  --rc-fs: clamp(0.9375rem, .34vw + 0.86875rem, 1.0375rem);

  /* The furniture unit. A reader who raises their font size wants the *writing*
     bigger; the running head, the citation line, the tag chips, the two onward
     buttons and the two chrome bars are furniture, and letting those grow at the
     same rate spends the reader's own request against them — every pixel the
     furniture gains comes straight out of the page window the writing lives in.
     Measured: at a 26px root, growing everything together shrank that window
     from 283px to 91px and turned a four-page card into eighteen.

     So furniture follows at 40 per cent. `0.4rem + 9.6px` is exactly 16px at the
     default root size, which makes every declaration below byte-identical to
     what shipped, and 20px rather than 26px at a 26px root. It grows — a reader
     who needs bigger text needs a legible running head too — just not at the
     expense of the thing they were actually trying to read. */
  --rl-fur: calc(0.4rem + 9.6px);
  --rc-gap: calc(2.75 * var(--rl-fur));
}

/* the chrome bars, the card's own head and foot, and the two onward rows */
.rl-scope, .rl-count { font-size: calc(.68 * var(--rl-fur)); }
.rl-barbtn { font-size: calc(.66 * var(--rl-fur)); }
.rl-tag { font-size: calc(.69 * var(--rl-fur)); }
.rc-run { font-size: calc(.6 * var(--rl-fur)); }
.rc .rc-kick { font-size: calc(.63 * var(--rl-fur)); }
.rc-f { font-size: calc(.62 * var(--rl-fur)); }
.rl-next-l { font-size: calc(.58 * var(--rl-fur)); }
.rl-go-k { font-size: calc(.74 * var(--rl-fur));
           min-height: calc(1.92 * var(--rl-fur)); }
.rl-go-s { font-size: calc(.55 * var(--rl-fur)); }
/* `.rc-head` is deliberately absent. The headline is writing, not furniture, so
   it scales with the reader's request like the body text does. */

/* The slide's gutters come from the measured height of the two chrome bars
   rather than from a constant, because a bar holds text and text grows. The
   fallbacks are the shipped numbers, so a slide is still right if the
   measurement has not run yet. */
.rl-slide {
  padding-top: calc(var(--rl-topgap, 46px) + 6px);
  padding-bottom: calc(var(--rl-botgap, 60px) + 8px);
}

/* ...which only works if the bars are allowed to be as tall as their contents.
   The top bar was a fixed `height`, so at a large font size its own text spilled
   out of it. Both floors are written as literal rem here and not as
   `var(--rl-top)`: JS measures these boxes and writes the result back out, and
   a floor that referred to the variable it feeds would be a loop. */
.rl-top { height: auto;
          min-height: calc(2.875 * var(--rl-fur) + env(safe-area-inset-top, 0px)); }
.rl-bar { min-height: calc(3.75 * var(--rl-fur) + env(safe-area-inset-bottom, 0px)); }

/* --------------------------------------------------------------------------
   A SHORT WINDOW, WHICH IS WHAT ZOOM MAKES
   Zooming to 200% on a laptop does not give the page a bigger font. It gives it
   half the CSS pixels: a 900px-tall window becomes 450. Measured there, the
   slide was handing 233px of its 330 to the two onward rows and leaving the card
   97px — so `.rc-body`, the flex child that absorbs the shortfall, collapsed to
   *zero height* and the citation and folio were clipped 158px below the visible
   bottom of the card. The reader lost the text, the source and the page count at
   once, which is the whole of what Ritesh reported.

   Flexbox had no way to know which of those boxes mattered. This says so. The
   rows yield in order of what a reader loses by not having them — the tag chips
   first, then the two named onward buttons, both of which are reachable from the
   index — and the writing, its source and its page number never yield at all.

   The thresholds are the measured ones: the card's own furniture and three
   readable lines need about 315px, both rows and their gaps cost about 233, so
   below roughly 660px of window the tags have to go and below 600 the onward
   row does too.
   -------------------------------------------------------------------------- */
@media (max-height: 660px) { .rl-tags { display: none; } }
@media (max-height: 600px) { .rl-next { display: none; } }
/* The two bars hold one line of small mono each, so 46 and 60 is generous
   spacing rather than needed height — and on a 450px window that generosity is
   27% of everything there is. Slimmed here, they hand about 34px back to the
   writing, which is the difference between two readable lines and four. The
   measured-gutter code picks the new heights up on its own. */
@media (max-height: 620px) {
  .rl-top { min-height: calc(2.1 * var(--rl-fur) + env(safe-area-inset-top, 0px)); }
  .rl-bar { min-height: calc(2.4 * var(--rl-fur) + env(safe-area-inset-bottom, 0px));
            padding-top: 5px; padding-bottom: calc(5px + env(safe-area-inset-bottom, 0px)); }
}
/* The picture shrinks rather than disappearing: a card without its animation is
   a worse card than a card with a small one. Two classes, not one — the art
   stylesheet is concatenated after this block and its own `.rc-art` floor of
   56px would otherwise win the tie on source order. */
@media (max-height: 560px) {
  .rc .rc-art { height: clamp(32px, 7vh, 52px); margin: 2px 0 4px; }
}

/* At 250% zoom a laptop window is about 360px and the card's fixed parts alone
   overran it: measured, the space left for text came to *minus six pixels*, so
   flexbox gave the body nothing and then pushed the citation out through the
   bottom of the card. Nothing here is optional at that point, so this frees
   about 100px — four readable lines — from the four places where it costs the
   reader least. */
@media (max-height: 470px) {
  /* The running head goes first because it costs the least: the part and section
     it names are already in the bar across the top of the screen, which makes it
     the one thing on a card genuinely duplicated elsewhere. The page folio is in
     the footer, not here, so it survives. */
  .rc-run { display: none; }
  /* The headline stays the biggest thing on the card, just less big, and sits
     closer to the text it introduces. */
  .rc .rc-head { font-size: clamp(1rem, 1.1vw + .5rem, 1.18rem);
                 margin-bottom: 7px; }
  /* A tighter box, and the citation sits closer to the words it belongs to. */
  .rc { padding: 12px 18px 9px; }
  .rc-f { margin-top: 8px; }
  /* The slide's breathing room above and below the card is the cheapest space
     on the screen at this height — the bars are already measured exactly, so
     this is pure margin. */
  .rl-slide {
    padding-top: calc(var(--rl-topgap, 46px) + 2px);
    padding-bottom: calc(var(--rl-botgap, 60px) + 3px);
  }
  /* The picture is deliberately not in this list. It is the only thing on a card
     that cannot be read anywhere else, and a reader at 250% zoom has not stopped
     caring what the card looks like. */
}

/* Scroll mode, and this rule is what makes it real rather than cosmetic.
   `data-spill` is set by the engine when a window is too short to page, and
   without this the body was still a multicolumn container: it went on breaking
   the text into columns off to the right, where `overflow-x: hidden` swallowed
   them. Measured on a 360px window, the flow was 4366px wide and the closing
   paragraph sat 3150px past the edge of the card — clean-looking and unreadable,
   which is the worse of the two failures. So the columns come off, the flow is
   allowed to grow as tall as the words need, and the body scrolls it.

   The height is keyed off the attribute rather than the media query on purpose:
   the engine decides this per card from what it actually measured, and a card can
   land in scroll mode at heights this query knows nothing about. */
.rc[data-spill="1"] .rc-flow {
  column-width: auto;
  column-count: 1;
  height: auto;
  transform: none;
}
.rc[data-spill="1"] .rc-body { overflow-y: auto; }

/* And this line is what stops the two modes arguing with each other. `.rc-body`
   sizes from its content, so once the flow above is allowed to grow to its full
   1376px the body's flex basis changes and it settles 28px taller than it was in
   page mode. That is enough to straddle the five-line threshold, so the engine
   measured 105px and chose scrolling, the card relaid out at 133px, the next pass
   measured 133px and chose paging, and it went round again — a card flipping
   between scrolling and twelve pages depending on which pass you caught it on.
   Pinning the basis to zero makes the body's share pure free space, which is what
   page mode already resolves to, so both modes now measure the same number and
   the decision holds still. */
.rc[data-spill="1"] .rc-body { flex: 1 1 0%; min-height: 0; }

/* A bullet may be split across a page turn. The base stylesheet forbids it, and
   at the default size that is the better call — a two-line bullet always fits
   whatever is left of the column, so refusing to cut it costs nothing and reads
   better. Enlarge the text, though, and the same bullet is five lines, no longer
   fits the space remaining, and the whole thing jumps to the next page: the
   reader zooms in for bigger words and gets a card with a hole in the middle of
   it and the words moved somewhere else. That is the opposite of the point.

   Splitting is the cheaper failure here, and it is barely a failure at all — this
   is already a paged surface where ordinary paragraphs run across page turns, so
   a bullet doing the same is consistent rather than novel, and `orphans: 2;
   widows: 2` on `li` still guarantees at least two lines land on each side of the
   cut. Quotes get the same treatment for the same reason. Genuinely atomic things
   — a figure, a diagram, a table row, a labelled panel — stay unbreakable, since
   those carry no meaning once halved.

   `.rc-flow` is written twice on purpose. The rule being overridden uses `:is()`
   with class arguments, which lifts it to three-class specificity; repeating the
   class is the plain way to clear that without inventing a selector. */
.rc .rc-flow.rc-flow :is(li, blockquote) { break-inside: auto; }

/* ==========================================================================
   THE ART
   One small scene of stick people per card, in its own row between the card's
   header and its body. `flex: 0 0 auto` keeps it out of the paging engine, so a
   card that runs to several pages keeps its picture fixed while the text turns.
   ========================================================================== */
.rc-art {
  flex: 0 0 auto;
  display: flex; align-items: center; justify-content: center;
  height: clamp(56px, 9.5vh, 88px);
  margin: clamp(2px, .6vw, 7px) 0 clamp(4px, .9vw, 10px);
}
.rc-art svg { height: 100%; width: 100%; max-width: 340px; overflow: visible; }

/* The whole vocabulary is strokes, so colour and weight are set once here. */
.art .fig { fill: none; stroke: var(--ink-soft); stroke-width: 1.9;
            stroke-linecap: round; stroke-linejoin: round; }
.art .fig circle { fill: var(--bg-elev); }
/* Part XXVII: the dog. Same stroke weight and same colour as a person on purpose —
   the part argues that another species does a social job, and drawing it fainter or
   thinner would argue the opposite in the margin of every card. The head fills like
   a person's head does, so the two bodies belong to one vocabulary. */
.art .dg { fill: none; stroke: var(--ink-soft); stroke-width: 1.9;
           stroke-linecap: round; stroke-linejoin: round; }
.art .dg circle { fill: var(--bg-elev); }
/* Part XXII: which people in a crowd a finding is about. A warm dot above the
   head was the first attempt and it vanishes at 240 units wide; the head is the
   largest single mark a figure owns, so the head is what gets filled. Same
   specificity as the rule above, so this has to stay below it. */
.art .fig-hit circle { fill: var(--warm-bright); stroke: var(--warm-bright); }
.art .thr { fill: none; stroke: var(--warm); stroke-width: 1.5;
            stroke-linecap: round; opacity: .75; }
.art .hrt { fill: var(--warm-bright); stroke: none; }
.art .spk { fill: none; stroke: var(--cool-bright); stroke-width: 1.5;
            stroke-linecap: round; }
/* Part XXV: a sung note. Warm and filled, like every arriving good thing in this
   vocabulary and unlike `spk`, which is cool because it means pain. */
/* Part XXVI. Two new marks; the third was already here. Laughter reuses Part V's
   `laf`, because a mark's meaning belongs to the whole ledger. `drp` is the one
   filled mark in here that is cool, because the tear is the sadness and only the
   response to it is warm. `mth` is a bare arc drawn beside a figure, never on its
   face, since a head is 4.6 units across. */
/* Part V already owns the laughter mark (`laf`, three warm strokes). Part XXVI
   needs one variant it does not have: a laugh that is *produced* rather than
   triggered. Faint, not cool — `laf-off` already means "out of step with the other
   laugh" in Part V, and cool means pain everywhere in this ledger. Faint means
   "less real", which is exactly the claim. No animation: the card is about which
   laugh is genuine, and a pulse would make it about which is louder. */
.art .laf-fake { stroke: var(--ink-faint); opacity: .5; }
.art .drp path { fill: var(--cool-bright); stroke: none; opacity: .88; }
.art .drp-off path { fill: var(--ink-faint); opacity: .4; }
.art .mth path { fill: none; stroke: var(--warm-bright); stroke-width: 1.8;
                 stroke-linecap: round; opacity: .92; }
.art .mth-off path { stroke: var(--ink-soft); opacity: .55; }
.art .spk-faint { opacity: .28; }
.art .nt ellipse { fill: var(--warm-bright); stroke: none; opacity: .92; }
.art .nt path    { fill: none; stroke: var(--warm-bright); stroke-width: 1.4;
                   stroke-linecap: round; opacity: .92; }
.art .nt-off ellipse, .art .nt-off path { fill: var(--ink-faint);
                   stroke: var(--ink-faint); opacity: .4; }
.art .rng { fill: none; stroke: var(--warm); stroke-width: 1.2; }
.art .grd { fill: none; stroke: var(--line-strong); stroke-width: 1;
            stroke-dasharray: 2 5; opacity: .7; }
/* A cell wall — the only hard surface anywhere in this ledger. `grd` on its own is
   a 1px line at 12% white, which is correct for something to lean on and
   invisible for something you cannot get past, so this overrides all four of its
   properties. Heavier than a figure's stroke so it reads as a slab; duller in
   colour than one so the person between two of them stays the subject. */
.art .cel { stroke: var(--ink-faint); stroke-width: 2.6;
            stroke-dasharray: none; opacity: 1; }
/* A zero line — the axis a bar can point either way from. `dv`, the panel divider,
   was the obvious thing to reuse and it is a 1px dashed line at 10% white: it
   vanishes the instant a warm bar touches it, and a zero-line chart with no visible
   zero is just bars sitting at odd offsets. Solid, neutral and thin, because an axis
   is not a finding and must not compete with one. */
.art .zero { stroke: var(--ink-faint); stroke-width: 1.4;
             stroke-dasharray: none; opacity: .8; }
.art .pg  { fill: none; stroke: var(--ink-faint); stroke-width: 1.4;
            stroke-linecap: round; stroke-linejoin: round; }
.art .bl  { fill: var(--warm-bright); stroke: none; }
.art .dv  { stroke: var(--line-strong); stroke-width: 1; stroke-dasharray: 3 4;
            opacity: .6; }
.art .ecg { fill: none; stroke-width: 1.5; stroke-linecap: round;
            stroke-linejoin: round; }
.art .ecg.live  { stroke: var(--warm); opacity: .85; }
.art .ecg.dying { stroke: var(--cool); opacity: .55; }
.art .tk  { fill: none; stroke: var(--warm-bright); stroke-width: 2;
            stroke-linecap: round; stroke-linejoin: round; }

/* ---------- the motion --------------------------------------------------
   Slow, small, looping. Every animation below runs on a long cycle with a
   long pause built into its keyframes, so a card that sits on screen for a
   minute is not a card that twitches for a minute.
   ------------------------------------------------------------------------ */
@keyframes fadeUp   { 0%,8% { opacity: 0; transform: translateY(4px); }
                      26%,88% { opacity: 1; transform: translateY(0); }
                      100% { opacity: 0; transform: translateY(4px); } }
@keyframes drawIn   { 0%,14% { stroke-dashoffset: 130; opacity: 0; }
                      40%,88% { stroke-dashoffset: 0; opacity: .75; }
                      100% { stroke-dashoffset: 130; opacity: 0; } }
@keyframes beatIn   { 0%,52% { opacity: 0; transform: scale(.4); }
                      66% { opacity: 1; transform: scale(1.18); }
                      76%,90% { opacity: 1; transform: scale(1); }
                      100% { opacity: 0; transform: scale(.4); } }
@keyframes glowPulse{ 0%,100% { opacity: .92; } 50% { opacity: 1; } }
@keyframes dimOut   { 0%,18% { opacity: .85; transform: translateY(0); }
                      64%,100% { opacity: .1; transform: translateY(3px); } }
@keyframes leanIn   { 0%,100% { transform: translateX(0); }
                      45%,60% { transform: translateX(var(--lean, 4px)); } }
@keyframes tickIn   { 0%,58% { opacity: 0; stroke-dashoffset: 24; }
                      72%,92% { opacity: 1; stroke-dashoffset: 0; }
                      100% { opacity: 0; stroke-dashoffset: 24; } }
@keyframes toss     { 0%,6%   { transform: translate(0,0); }
                      44%,50% { transform: translate(100px,0); }
                      94%,100%{ transform: translate(0,0); } }
@keyframes sparkLate{ 0%,54% { opacity: 0; transform: scale(.5); }
                      68%,86% { opacity: .95; transform: scale(1); }
                      100% { opacity: 0; transform: scale(.5); } }
@keyframes sparkFade{ 0%,30% { opacity: .95; transform: scale(1); }
                      78%,100% { opacity: .12; transform: scale(.62); } }
@keyframes sparkPulse{0%,100%{ opacity: .35; transform: scale(.85); }
                      50% { opacity: .9; transform: scale(1); } }
@keyframes pillIn   { 0%,24% { transform: translateY(0); opacity: .95; }
                      60%,100% { transform: translateY(16px); opacity: 0; } }
@keyframes ringOut  { 0% { r: 12; opacity: .6; }
                      70%,100% { r: 34; opacity: 0; } }
@keyframes scanHead { 0%,100% { transform: rotate(0deg); }
                      30% { transform: rotate(-7deg); }
                      62% { transform: rotate(7deg); } }
@keyframes ache     { 0%,100% { transform: translateY(0); }
                      50% { transform: translateY(1.6px); } }
@keyframes ecgLive  { 0% { stroke-dashoffset: 220; } 100% { stroke-dashoffset: 0; } }
@keyframes giftOut  { 0%,8%   { transform: translate(0,0); opacity: 0; }
                      14% { opacity: 1; }
                      40%,46% { transform: translate(84px,0); opacity: 1; }
                      52%,100%{ transform: translate(84px,0); opacity: 0; } }
@keyframes giftBack { 0%,54%  { transform: translate(0,0); opacity: 0; }
                      60% { opacity: 1; }
                      86%,92% { transform: translate(-84px,0); opacity: 1; }
                      98%,100%{ transform: translate(-84px,0); opacity: 0; } }
@keyframes giftTaken{ 0%,10%  { transform: translate(0,0); opacity: 0; }
                      20% { opacity: 1; }
                      44%,100%{ transform: translate(-38px,0); opacity: 0; } }
@keyframes sever    { 0%,34%  { opacity: .8; }
                      70%,100%{ opacity: .12; } }
@keyframes turnAway { 0%,40%  { transform: scaleX(1); }
                      60%,100%{ transform: scaleX(-1); } }
@keyframes passBaby { 0%,6%   { transform: translateX(52px); }
                      28%,34% { transform: translateX(100px); }
                      56%,62% { transform: translateX(148px); }
                      84%,94% { transform: translateX(196px); }
                      100%    { transform: translateX(52px); } }
@keyframes foodOver { 0%,14%  { transform: translate(-30px,4px); opacity: 0; }
                      26% { opacity: 1; }
                      58%,72% { transform: translate(0,0); opacity: 1; }
                      86%,100%{ transform: translate(24px,6px); opacity: 0; } }
@keyframes lifted   { 0%,100% { transform: translateY(0); }
                      50% { transform: translateY(-2.4px); } }
@keyframes gazeOut  { 0%,12%  { stroke-dashoffset: 150; opacity: 0; }
                      24% { opacity: .8; }
                      52%,84% { stroke-dashoffset: 0; opacity: .8; }
                      100%    { stroke-dashoffset: 150; opacity: 0; } }
@keyframes landOn   { 0%,48%  { opacity: .2; transform: scale(.7); }
                      62%,86% { opacity: 1; transform: scale(1); }
                      100%    { opacity: .2; transform: scale(.7); } }
/* Giving costs. The two halves of that sentence, as two opposite fades. */
@keyframes spendOut { 0%,20%   { opacity: 1; }
                      58%,88%  { opacity: .34; }
                      100%     { opacity: 1; } }
@keyframes gainUp   { 0%,20%   { opacity: .34; }
                      58%,88%  { opacity: 1; }
                      100%     { opacity: .34; } }
@keyframes handOff  { 0%,20%   { transform: translateX(-42px); opacity: 0; }
                      32%      { opacity: 1; }
                      58%,88%  { transform: translateX(42px); opacity: 1; }
                      96%,100% { transform: translateX(42px); opacity: 0; } }
/* Part IV: the machinery. */
@keyframes driftUp  { 0%,10%   { opacity: 0; transform: translateY(8px); }
                      30%,66%  { opacity: .8; transform: translateY(0); }
                      100%     { opacity: 0; transform: translateY(-7px); } }
/* Every figure in `in-step` shares this, undelayed. The unison is the finding. */
@keyframes unison   { 0%,100% { transform: translateY(0); }
                      50%     { transform: translateY(-3px); } }
@keyframes dimSome  { 0%,22%   { opacity: .9; }
                      60%,88%  { opacity: .58; }
                      100%     { opacity: .9; } }
@keyframes mistOut  { 0%,16%   { opacity: 0; transform: translate(0,0); }
                      34%      { opacity: .9; }
                      100%     { opacity: 0; transform: translate(9px,-7px); } }
/* draws most of the way, then gives up and goes out without ever arriving */
@keyframes failShort{ 0%,12%   { stroke-dashoffset: 90; opacity: 0; }
                      26%      { opacity: .7; }
                      54%      { stroke-dashoffset: 26; opacity: .7; }
                      74%,100% { stroke-dashoffset: 26; opacity: 0; } }
@keyframes cellUp   { 0%,100% { opacity: .55; transform: scale(.8); }
                      50%     { opacity: 1; transform: scale(1.18); } }
@keyframes cellDown { 0%,20%  { opacity: .7; }
                      64%,100%{ opacity: .16; } }
@keyframes ringGone { 0%,14%  { opacity: .5; transform: scale(1); }
                      70%,100%{ opacity: 0; transform: scale(1.3); } }
@keyframes swellUp  { 0%,14%  { opacity: .25; transform: scale(.5); }
                      70%,100%{ opacity: .85; transform: scale(1.35); } }
@keyframes lookOver { 0%,18%  { transform: rotate(0deg); }
                      46%,84% { transform: rotate(11deg); }
                      100%    { transform: rotate(0deg); } }
/* Part V: the signals. */
/* The slowest loop in the set, because the speed is the finding. One to ten
   centimetres a second is what the nerve wants, so the hand takes its time. */
@keyframes strokeAcross { 0%,6%   { transform: translateX(0); opacity: 0; }
                          14%     { opacity: 1; }
                          74%     { transform: translateX(110px); opacity: 1; }
                          86%,100%{ transform: translateX(110px); opacity: 0; } }
@keyframes trailAfter   { 0%,8%   { stroke-dashoffset: 112; opacity: 0; }
                          18%     { opacity: .85; }
                          74%,88% { stroke-dashoffset: 0; opacity: .85; }
                          100%    { stroke-dashoffset: 112; opacity: 0; } }
/* early season, then later season — the two halves never share the screen */
@keyframes touchFirst   { 0%,4%   { opacity: 0; transform: scale(.4); }
                          12%,26% { opacity: 1; transform: scale(1); }
                          38%,100%{ opacity: 0; transform: scale(.4); } }
@keyframes ballAlong    { 0%,44%  { transform: translateX(0); opacity: 0; }
                          50%     { opacity: 1; }
                          88%     { transform: translateX(160px); opacity: 1; }
                          96%,100%{ transform: translateX(160px); opacity: 0; } }
@keyframes tryHard      { 0%,6%   { opacity: 0; transform: translateY(3px); }
                          18%,44% { opacity: 1; transform: translateY(0); }
                          56%,100%{ opacity: 0; transform: translateY(0); } }
@keyframes turnOff      { 0%,52%  { opacity: 0; transform: scaleX(1); }
                          64%,100%{ opacity: .8; transform: scaleX(-1); } }
@keyframes screenOff    { 0%,22%  { opacity: .85; }
                          58%,100%{ opacity: .08; } }
@keyframes strengthen   { 0%,22%  { opacity: .28; stroke-width: .9; }
                          62%,100%{ opacity: .9; stroke-width: 2.6; } }
@keyframes sndIn        { 0%,30%  { opacity: 0; }
                          62%,90% { opacity: .8; }
                          100%    { opacity: .35; } }
@keyframes lensOver     { 0%,10%  { transform: translateX(-26px); opacity: 0; }
                          24%     { opacity: .9; }
                          52%,88% { transform: translateX(0); opacity: .9; }
                          100%    { transform: translateX(22px); opacity: 0; } }
@keyframes tinyFound    { 0%,50%  { opacity: 0; transform: scale(.5); }
                          66%,88% { opacity: .95; transform: scale(1); }
                          100%    { opacity: 0; transform: scale(.5); } }
@keyframes phraseIn     { 0%,4%   { stroke-dashoffset: 28; opacity: 0; }
                          10%     { opacity: .8; }
                          24%,84% { stroke-dashoffset: 0; opacity: .8; }
                          100%    { stroke-dashoffset: 28; opacity: 0; } }
/* the laugh only ever arrives after its phrase has finished drawing */
@keyframes lafAfter     { 0%,26%  { opacity: 0; transform: translateY(2px); }
                          36%,80% { opacity: 1; transform: translateY(0); }
                          100%    { opacity: 0; transform: translateY(2px); } }
@keyframes lafPulse     { 0%,100% { opacity: .5; transform: translateY(1px); }
                          50%     { opacity: 1; transform: translateY(-1.5px); } }
/* Part VI: the arc. */
/* the ring that does not break — deliberately the opposite of `ringGone` */
@keyframes ringHold     { 0%,100% { opacity: .3; } 50% { opacity: .6; } }
/* A short bright window, so eight of these on staggered delays read as one
   person awake travelling along a row of sleepers rather than a row lighting up
   together. Everything else in this file that fades in holds its bright state for
   most of the loop, which is exactly wrong for a rota. */
@keyframes onWatch      { 0%,4%   { opacity: 0; }
                          11%,17% { opacity: 1; }
                          24%,100%{ opacity: 0; } }
@keyframes flipSide     { 0%,44%  { transform: translateX(-58px); }
                          56%,100%{ transform: translateX(58px); } }
/* a few steps away and back. Small on purpose: it is a test, not an abandoning */
@keyframes leaveReturn  { 0%,12%  { transform: translateX(0); }
                          40%,56% { transform: translateX(46px); }
                          88%,100%{ transform: translateX(0); } }
@keyframes backOn       { 0%,14%  { opacity: .8; }
                          40%,56% { opacity: .12; }
                          84%,100%{ opacity: .8; } }
/* Part VII: the pair. */
/* swells and settles back to exactly where it started — the average trajectory */
@keyframes swellBack    { 0%,10%  { transform: scale(.5); opacity: .55; }
                          38%,50% { transform: scale(1); opacity: 1; }
                          82%,100%{ transform: scale(.5); opacity: .55; } }
/* rotating about the feet, so the head goes out over the drop */
@keyframes tipOut       { 0%,16%  { transform: rotate(0deg); }
                          48%,72% { transform: rotate(18deg); }
                          100%    { transform: rotate(0deg); } }
/* the outer ring leaves outwards under its own steam — nothing cuts it */
@keyframes letGo        { 0%,18%  { opacity: .6; transform: translateX(0); }
                          72%,100%{ opacity: 0;
                                    transform: translateX(var(--go, 20px)); } }
/* Part VIII: the many. */
/* the eye opens before the tie crosses, never after */
@keyframes watchOn      { 0%,8%   { opacity: .15; }
                          22%,88% { opacity: 1; }
                          100%    { opacity: .15; } }
/* like `failShort` but in percent of the path, so the shortfall is reliable
   whatever length the arc happens to be. Stops at 54 per cent of the way. */
@keyframes giveUpHalf   { 0%,12%   { stroke-dashoffset: 100; opacity: 0; }
                          26%      { opacity: .7; }
                          54%      { stroke-dashoffset: 46; opacity: .7; }
                          74%,100% { stroke-dashoffset: 46; opacity: 0; } }
/* Part IX: the rupture. */
/* a measurement drawing itself left to right, in percent of its own length */
@keyframes drawTrace    { 0%,8%   { stroke-dashoffset: 100; opacity: 0; }
                          30%,88% { stroke-dashoffset: 0; opacity: .9; }
                          100%    { stroke-dashoffset: 100; opacity: 0; } }
/* one way, and it stays there — unlike `leaveReturn`, nobody comes back */
@keyframes joinIn       { 0%,14%  { transform: translateX(0); }
                          52%,88% { transform: translateX(62px); }
                          100%    { transform: translateX(0); } }
/* Part XXIV: the same arrival walked the other way. `joinIn` only ever travels
   right, and the closing scene of the part needs somebody coming *in* to a place
   that is on their left — pointed the wrong way they walk off the canvas, which is
   exactly what the first version did. */
@keyframes joinInL      { 0%,14%  { transform: translateX(0); }
                          52%,88% { transform: translateX(-62px); }
                          100%    { transform: translateX(0); } }
/* Part X: the repair. */
/* A short handover: ±13px about the midpoint, so it starts in one person's hand
   and ends in the other's. `handOff` swings ±42px, which is right for a wide
   frame and lands outside the figures entirely in a half-width station. The
   travel here is small on purpose — the thing crosses a gap, it does not fly. */
@keyframes passOver     { 0%,16%  { transform: translateX(-13px); opacity: .45; }
                          34%     { opacity: 1; }
                          62%,88% { transform: translateX(13px); opacity: 1; }
                          100%    { transform: translateX(-13px); opacity: .45; } }

.art .fade-in   { animation: fadeUp 9s ease-in-out infinite both; }
.art .thr-in    { stroke-dasharray: 130;
                  animation: drawIn 9s ease-in-out infinite both; }
.art .beat-in   { transform-box: fill-box; transform-origin: center;
                  animation: beatIn 9s ease-in-out infinite both; }
.art .beat-slow { transform-box: fill-box; transform-origin: center;
                  animation: beatIn 7s ease-in-out infinite both; }
.art .glow      { animation: glowPulse 4s ease-in-out infinite; }
.art .glow path, .art .glow circle { stroke: var(--warm-ink); }
.art .dim-out   { transform-box: fill-box;
                  animation: dimOut 7s ease-in-out infinite both; }
.art .steady    { animation: fadeUp 11s ease-in-out infinite both; }
.art .lonely    { opacity: .3; animation: ache 5s ease-in-out infinite; }
.art .faint     { opacity: .32; }
.art .ache      { transform-box: fill-box;
                  animation: ache 4.5s ease-in-out infinite; }
.art .lean-r    { --lean: 7px; transform-box: fill-box;
                  animation: leanIn 7s ease-in-out infinite; }
.art .lean-l    { --lean: -7px; transform-box: fill-box;
                  animation: leanIn 7s ease-in-out infinite; }
.art .tk        { stroke-dasharray: 24;
                  animation: tickIn 7s ease-in-out infinite both; }
.art .toss      { animation: toss 5s cubic-bezier(.45,0,.55,1) infinite; }
.art .excluded  { opacity: .55; animation: ache 5s ease-in-out infinite; }
.art .spk-late  { transform-box: fill-box; transform-origin: center;
                  animation: sparkLate 5s ease-in-out infinite both; }
.art .spk-fade  { transform-box: fill-box; transform-origin: center;
                  animation: sparkFade 6s ease-in-out infinite both; }
.art .spk-pulse { transform-box: fill-box; transform-origin: center;
                  animation: sparkPulse 3.5s ease-in-out infinite; }
.art .pill      { fill: none; stroke: var(--warm-bright); stroke-width: 1.6;
                  stroke-linecap: round;
                  animation: pillIn 6s ease-in-out infinite both; }
.art .rng-out   { animation: ringOut 3.6s ease-out infinite both; }
.art .scan circle { transform-box: fill-box; transform-origin: 50% 120%;
                    animation: scanHead 6s ease-in-out infinite; }
.art .want      { transform-box: fill-box;
                  animation: leanIn 6s ease-in-out infinite; --lean: 5px; }
.art .thr-live  { opacity: .8; }
.art .thr-cut   { stroke: var(--cool); stroke-dasharray: 4 5; opacity: .5; }
.art .thr-thick { stroke-width: 2.4; opacity: .9; }
.art .thr-thin  { stroke-width: .9; opacity: .3; }
.art .thr-sever { stroke-dasharray: 4 5;
                  animation: sever 7s ease-in-out infinite both; }
/* Part XIV: a tie that cannot exist before the person who made it arrives.
   `thr-in` draws on a schedule of its own — 14% to 40% — and `joinIn` does not
   land its walker until 52%, so the two together hang a gold arc in the air
   beside a figure that is still walking. Same period, phase fitted to the
   arrival: nothing until 52%, drawn by 64%, gone before they walk back. */
@keyframes drawOnArrival { 0%,52%   { stroke-dashoffset: 130; opacity: 0; }
                           64%,84%  { stroke-dashoffset: 0; opacity: .75; }
                           92%,100% { stroke-dashoffset: 0; opacity: 0; } }
.art .thr-arrive { stroke-dasharray: 130;
                   animation: drawOnArrival 9s ease-in-out infinite both; }
.art .gift-out  { fill: var(--warm-bright); stroke: none;
                  animation: giftOut 7s ease-in-out infinite both; }
.art .gift-back { fill: var(--warm-bright); stroke: none;
                  animation: giftBack 7s ease-in-out infinite both; }
.art .gift-taken{ fill: var(--cool-bright); stroke: none;
                  animation: giftTaken 7s ease-in-out infinite both; }
.art .taker     { transform-box: fill-box; transform-origin: center; }
.art .taker path, .art .taker circle { stroke: var(--cool); }
.art .food      { fill: var(--warm-bright); stroke: none;
                  animation: foodOver 7s ease-in-out infinite both; }
.art .small     { opacity: .95; }
.art .give      { transform-box: fill-box;
                  animation: leanIn 7s ease-in-out infinite; --lean: 5px; }
/* `.art .fig circle` fills the head with the card background, and a cradled baby
   is a circle inside that same group — so it needs to outrank it, or the bead
   renders in the background colour and vanishes. */
.art .fig .bby, .art .bby, .art .passed { fill: var(--warm-bright); stroke: none; }
.art .passed    { animation: passBaby 11s ease-in-out infinite both; }
.art .passed path { stroke: var(--warm); stroke-width: 1.4;
                    stroke-linecap: round; fill: none; }
.art .helper    { transform-box: fill-box;
                  animation: lifted 4s ease-in-out infinite; }
.art .dropped   { fill: var(--ink-faint); stroke: none; }
.art .skull     { fill: none; stroke: var(--ink-faint); stroke-width: 1.6;
                  stroke-linejoin: round; opacity: .8; }
.art .gaze      { fill: none; stroke: var(--warm); stroke-width: 1.4;
                  stroke-dasharray: 3 4; stroke-linecap: round;
                  animation: gazeOut 6s ease-in-out infinite both; }
.art .target    { fill: var(--warm-bright); stroke: none;
                  animation: landOn 6s ease-in-out infinite both; }
.art .looker circle { transform-box: fill-box; transform-origin: 50% 130%;
                      animation: lookOver 6s ease-in-out infinite; }
.art .cl        { fill: var(--ink-soft); stroke: none; }
.art .drift     { animation: driftUp 8s ease-in-out infinite both; }
.art .sync      { animation: unison 2.4s ease-in-out infinite; }
.art .thr-dim   { animation: dimSome 6s ease-in-out infinite; }
.art .bottle    { fill: none; stroke: var(--ink-faint); stroke-width: 1.5;
                  stroke-linejoin: round; opacity: .8; }
.art .mist      { fill: var(--cool-bright); stroke: none;
                  animation: mistOut 5s ease-in-out infinite both; }
.art .thr-fail  { stroke: var(--cool); stroke-dasharray: 90;
                  animation: failShort 7s ease-in-out infinite both; }
.art .cell-up   { fill: var(--warm-bright); transform-box: fill-box;
                  transform-origin: center;
                  animation: cellUp 4.5s ease-in-out infinite; }
.art .cell-dn   { fill: var(--cool); animation: cellDown 7s ease-in-out infinite both; }
.art .rng-broken{ stroke-dasharray: 5 6; transform-box: fill-box;
                  transform-origin: center;
                  animation: ringGone 6s ease-in-out infinite both; }
.art .swell     { transform-box: fill-box; transform-origin: center;
                  animation: swellUp 7s ease-in-out infinite both; }
.art .spend     { animation: spendOut 6s ease-in-out infinite; }
.art .gain      { opacity: .34; animation: gainUp 6s ease-in-out infinite; }
.art .hand-off  { fill: var(--warm-bright); stroke: none;
                  animation: handOff 6s ease-in-out infinite both; }
.art .tool      { fill: none; stroke: var(--warm-bright); stroke-width: 1.7;
                  stroke-linecap: round;
                  animation: fadeUp 9s ease-in-out infinite both; }
.art .tool-fade { stroke: var(--cool); opacity: .3; animation: none; }
.art .bub       { fill: none; stroke: var(--ink-faint); stroke-width: 1.4;
                  stroke-linejoin: round; opacity: .75; }
.art .ecg.live  { stroke-dasharray: 220;
                  animation: ecgLive 6s linear infinite; }
/* the signals */
.art .snd       { fill: none; stroke: var(--warm); stroke-width: 1.3;
                  stroke-linecap: round; opacity: .55; }
.art .laf       { fill: none; stroke: var(--warm-bright); stroke-width: 1.6;
                  stroke-linecap: round; }
.art .skin      { fill: none; stroke: var(--ink-soft); stroke-width: 1.6;
                  stroke-linecap: round; opacity: .45; }
.art .trail     { stroke-width: 2.4; stroke-dasharray: 112;
                  animation: trailAfter 13s ease-in-out infinite both; }
.art .hand      { fill: var(--warm-bright); stroke: none; }
.art .stroking  { animation: strokeAcross 13s linear infinite both; }
.art .touch-first { fill: var(--warm); transform-box: fill-box;
                    transform-origin: center;
                    animation: touchFirst 8s ease-in-out infinite both; }
.art .ball-along{ animation: ballAlong 8s ease-in-out infinite both; }
.art .try-hard  { transform-box: fill-box;
                  animation: tryHard 9s ease-in-out infinite both; }
.art .turn-off  { transform-box: fill-box; transform-origin: center;
                  animation: turnOff 9s ease-in-out infinite both; }
.art .screen    { fill: none; stroke: var(--ink-faint); stroke-width: 1.6;
                  animation: screenOff 8s ease-in-out infinite both; }
.art .strengthen{ stroke: var(--warm);
                  animation: strengthen 8s ease-in-out infinite both; }
.art .snd-in    { animation: sndIn 8s ease-in-out infinite both; }
.art .lens      { animation: lensOver 9s ease-in-out infinite both; }
.art .lens-r    { fill: none; stroke: var(--ink-soft); stroke-width: 1.7; }
.art .lens-h    { fill: none; stroke: var(--ink-soft); stroke-width: 2.2;
                  stroke-linecap: round; }
.art .tiny      { fill: var(--warm-bright); transform-box: fill-box;
                  transform-origin: center;
                  animation: tinyFound 9s ease-in-out infinite both; }
.art .phrase    { fill: none; stroke: var(--ink-soft); stroke-width: 2.2;
                  stroke-linecap: round; stroke-dasharray: 28;
                  animation: phraseIn 9s ease-in-out infinite both; }
.art .laf-beat  { transform-box: fill-box;
                  animation: lafAfter 9s ease-in-out infinite both; }
/* both of these share one phase, undelayed: the match is the whole finding */
.art .laf-sync  { transform-box: fill-box;
                  animation: lafPulse 2.6s ease-in-out infinite; }
/* and these two are deliberately out of step with each other */
.art .laf-off   { transform-box: fill-box; stroke: var(--cool-bright);
                  animation: lafPulse 3.4s ease-in-out infinite; }
.art .laf-off2  { transform-box: fill-box; stroke: var(--cool-bright);
                  animation: lafPulse 2.1s ease-in-out infinite;
                  animation-delay: -1.1s; }
/* the arc */
.art .rng-hold  { animation: ringHold 6s ease-in-out infinite; }
/* the night: one bright figure travelling along a row of dim ones */
.art .watch-turn { opacity: 0; animation: onWatch 9s linear infinite both; }
.art .coin      { animation: flipSide 5s ease-in-out infinite; }
.art .coin circle { fill: var(--ink-faint); }
.art .leaves    { animation: leaveReturn 9s ease-in-out infinite both; }
.art .back-on   { stroke-width: 2.2;
                  animation: backOn 9s ease-in-out infinite both; }
.art .ledge     { fill: none; stroke: var(--line-strong); stroke-width: 1.6;
                  stroke-linecap: round; stroke-linejoin: round; opacity: .8; }
.art .tip-out   { transform-box: fill-box; transform-origin: 50% 100%;
                  animation: tipOut 6s ease-in-out infinite; }
.art .kin       { fill: var(--warm-bright); stroke: none; }
/* the pair */
.art .cool-mark { fill: var(--cool-bright); stroke: none; }
.art .swell-back{ transform-box: fill-box; transform-origin: center;
                  animation: swellBack 8s ease-in-out infinite both; }
.art .let-go    { animation: letGo 9s ease-in-out infinite both; }
/* the many */
/* heavier and flatter than `grd`: this is a road whose *length* is the point */
.art .road      { fill: none; stroke: var(--line-strong); stroke-width: 3.4;
                  stroke-linecap: round; opacity: .75; }
.art .watch     { animation: watchOn 9s ease-in-out infinite both; }
.art .thr-give  { stroke: var(--cool); stroke-dasharray: 100;
                  animation: giveUpHalf 7s ease-in-out infinite both; }
/* the rupture */
.art .tr        { fill: none; stroke: var(--warm); stroke-width: 1.8;
                  stroke-linecap: round; stroke-linejoin: round; }
.art .tr-cool   { stroke: var(--cool); }
/* where a measurement should have gone, had nothing happened to it */
.art .tr-ghost  { stroke: var(--ink-faint); stroke-width: 1.3;
                  stroke-dasharray: 3 4; opacity: .55; }
.art .tr-draw   { stroke-dasharray: 100;
                  animation: drawTrace 9s ease-in-out infinite both; }
/* Part XXIII: the disaster. Cool and heavy, because it is the only mark here that
   nobody chose and nobody can lean on. `wv-sweep` draws it across the frame once
   per cycle so the arc *arrives* — an arc that is simply already there reads as a
   hill, and the people under it stop being people something happened to. */
.art .wv        { fill: none; stroke: var(--cool); stroke-width: 2.6;
                  stroke-linecap: round; opacity: .5; }
.art .wv-sweep  { stroke-dasharray: 100;
                  animation: drawTrace 9s ease-in-out infinite both; }
/* the second disaster in a two-panel scene: same arc, drawn at the same moment, so
   nothing in the timing suggests one of them came first */
.art .wv-aimed  { opacity: .68; }
/* a screen that stays lit, as against `.screen`, which goes dark */
.art .scrn      { fill: none; stroke: var(--ink-faint); stroke-width: 1.6; }
.art .road-warm { stroke: var(--warm); opacity: .85; }
/* A bar that stands for no difference: a reference at 1.0, or an outcome that was
   looked for and measured as nothing. Plain `road` is 16% white, which is legible
   when it is the only mark in a frame and disappears the moment a warm bar is put
   beside it — so a card whose whole finding is "this one moved and that one did
   not" loses half its sentence. Neutral grey and not cool, because cool in this
   ledger marks the worse of two branches and a null result is not a bad one. */
.art .road-null { stroke: var(--ink-faint); opacity: .6; }
/* A measured loss. `road-warm` is a gain and `road-null` is nothing, which left no
   way to draw a bar that points the wrong way — and the ledger has plenty of those.
   Cool is already the worse branch everywhere else here (`tr-cool`, `ecg.dying`,
   `thr-cut`), so this borrows nothing new. */
.art .road-cool { stroke: var(--cool); opacity: .85; }
/* A point mark for a measurement that landed on zero. On a chart built around a
   zero line, a null result has no length to draw, and a short stub would read as a
   small effect — which is a different and wrong claim. Same colour and weight as
   `road-null` so the two say the same word. */
.art .dt-null   { fill: var(--ink-faint); stroke: none; opacity: .6; }
/* a `grd` that is a real surface rather than a suggestion of one */
.art .solid     { stroke-dasharray: none; opacity: .75; }
.art .join-in   { animation: joinIn 9s ease-in-out infinite both; }
.art .join-in-l { animation: joinInL 9s ease-in-out infinite both; }
.art .found     { transform-box: fill-box; transform-origin: 50% 100%;
                  animation: tinyFound 9s ease-in-out infinite both; }
.art .clump     { animation: fadeUp 9s ease-in-out infinite both; }
.art .pass-over { fill: var(--warm-bright); stroke: none;
                  animation: passOver 6s ease-in-out infinite both; }
/* Part XXIV: the same travel, as animation only, so it can be worn by something
   that already has its own fill — a plate of food crossing a table. Two
   directions, because the finding those carry is that the food goes both ways,
   and one arrow cannot say reciprocity. Drawing it instead as two crossing
   threads was the first attempt: they merged into one closed lens between the two
   heads and read as a rugby ball. */
   `passBack` already exists further down, declared for Part XIII — these reuse it
   rather than redeclaring, and the two `.pass-*` classes below carry no fill of
   their own precisely so a plate keeps its rim and its warm centre. */
.art .pass-r    { animation: passOver 6s ease-in-out infinite both; }
.art .pass-l    { animation: passBack 6s ease-in-out infinite both; }
/* Part XI: the long thread. */
.art .hut       { fill: none; stroke: var(--ink-faint); stroke-width: 1.4; }
/* the one house that pulled away from the rest */
.art .hut-big   { stroke: var(--warm); stroke-width: 1.8; opacity: .95; }
/* Part XXIV: the table. The slab is opaque and heavier than a `grd` because it
   has to occlude the diners' legs — a table you can see through puts everybody's
   shins in the soup. Legs are thinner than the top so the top stays the subject. */
.art .tbl rect     { fill: var(--line-strong); stroke: none; opacity: .92; }
.art .tbl .tbl-leg { opacity: .5; }
/* A table nobody is at. Dimmed rather than dashed: a dashed table would read as
   a table not yet built, and the point of these scenes is that it is right there. */
.art .tbl-cold rect { opacity: .3; }
/* Food. Warm and filled, because in every other part of this vocabulary a filled
   warm mark is the good thing arriving. */
.art .plt circle       { fill: none; stroke: var(--warm); stroke-width: 1.3;
                         opacity: .85; }
.art .plt .plt-food    { fill: var(--warm-bright); stroke: none; opacity: .9; }
.art .plt-cold circle  { stroke: var(--ink-faint); opacity: .45; }
.art .plt-cold .plt-food { fill: var(--ink-faint); opacity: .45; }
/* Part XII: the room. The one place a colour does literal work in this
   vocabulary rather than carrying a meaning — a green circle is a tree. */
.art .leaf      { fill: none; stroke: var(--good); stroke-width: 1.5;
                  opacity: .8; }
/* An office partition. `wall()` paints with the ground stroke, which is faint
   and dashed on purpose — right for a floor, invisible for a wall. This is the
   same weight as a screen, because both are objects rather than surfaces. */
.art .partn     { stroke: var(--ink-faint); stroke-width: 1.6;
                  stroke-dasharray: none; opacity: .9; }
/* The ring that returned an answer nobody else's ring returned. Cool, not warm,
   because everywhere else in this ledger cool is the colour of a number you
   should not trust, and that is exactly what this one is. `watching()` puts its
   class on a wrapper, and `.rng` paints the circle directly, so this has to
   reach down to the circle — inheritance would lose. */
.art .rng-odd .rng { stroke: var(--cool); opacity: .95; }

/* Part XIII: the check. Two new marks only.
   `pass-back` is `pass-over` run the other way, for the one card in this ledger
   whose subject is that a thing causes the thing that caused it. It is cool
   because the return trip is the direction that complicates everything else
   here, and cool has meant exactly that since Part IX.
   `undecided` is the closing mark: something on a page that never settles. */
@keyframes passBack     { 0%,16%  { transform: translateX(13px); opacity: .45; }
                          34%     { opacity: 1; }
                          62%,88% { transform: translateX(-13px); opacity: 1; }
                          100%    { transform: translateX(13px); opacity: .45; } }
@keyframes undecided    { 0%,38%  { transform: translateX(-7px); }
                          50%,88% { transform: translateX(7px); }
                          100%    { transform: translateX(-7px); } }
.art .pass-back { fill: var(--cool-bright); stroke: none;
                  animation: passBack 6s ease-in-out infinite both; }
.art .undecided { fill: var(--warm-bright); stroke: none;
                  animation: undecided 7s ease-in-out infinite both; }
/* the two ends of a confidence interval, drawn dashed so neither reads as a
   measurement — and reusing `fadeUp` so they breathe rather than sit there */
.art .rng-range { stroke: var(--cool); stroke-dasharray: 3 4; opacity: .3;
                  animation: fadeUp 8s ease-in-out infinite both; }
/* A ring that was looked into and had nothing in it. Dashed, because dashed has
   meant "a boundary rather than a thing" since the first ground line, and this
   ring is a place somebody searched rather than anything that is there. Solid
   and faint was the first try and it vanished against the body behind it.
   It turns, at 24 seconds a revolution — far slower than anything else in this
   file. A search that found its answer stops. This one has not stopped. */
@keyframes searching    { 0%   { transform: rotate(0deg); }
                          100% { transform: rotate(360deg); } }
.art .empty     { stroke: var(--ink-soft); stroke-width: 1.4;
                  stroke-dasharray: 3 3; opacity: .75;
                  transform-box: fill-box; transform-origin: center;
                  animation: searching 24s linear infinite; }

/* Motion is a courtesy, not a requirement. With it turned off every scene has
   to still read as a finished drawing — which is why nothing above starts from
   `opacity: 0` in its resting state except where the still is complete anyway. */
@media (prefers-reduced-motion: reduce) {
  .art * { animation: none !important; }
  .art .fade-in, .art .steady, .art .beat-in, .art .beat-slow,
  .art .spk-late, .art .tk, .art .pill { opacity: 1 !important;
    transform: none !important; }
  .art .thr-in, .art .thr-arrive { stroke-dashoffset: 0 !important;
    opacity: .75 !important; }
  .art .dim-out { opacity: .18 !important; }
  /* the still has to say "one gave and now has less", so it holds mid-gesture */
  .art .spend { opacity: .34 !important; }
  .art .gain, .art .hand-off { opacity: 1 !important; transform: none !important; }
  /* the machinery: each still has to hold the half of the sentence it carries */
  .art .drift, .art .mist { opacity: .8 !important; transform: none !important; }
  .art .sync, .art .swell { transform: none !important; opacity: .85 !important; }
  .art .cell-up { opacity: 1 !important; transform: none !important; }
  .art .cell-dn { opacity: .18 !important; }
  .art .thr-dim { opacity: .6 !important; }
  .art .thr-fail { stroke-dashoffset: 26 !important; opacity: .5 !important; }
  .art .rng-broken { opacity: .3 !important; transform: none !important; }
  .art .spk-fade { opacity: .4 !important; }
  .art .rng-out { opacity: .28 !important; }
  .art .ecg.live { stroke-dasharray: none !important; }
  /* the signals: each still is frozen at the moment the finding is visible */
  .art .stroking { transform: translateX(66px) !important; opacity: 1 !important; }
  .art .trail { stroke-dashoffset: 46 !important; opacity: .85 !important; }
  .art .touch-first, .art .snd-in, .art .laf-beat, .art .laf-sync,
  .art .laf-off, .art .laf-off2, .art .tiny, .art .lens {
    opacity: .95 !important; transform: none !important; }
  .art .ball-along { transform: translateX(102px) !important; opacity: 1 !important; }
  /* the small one has already turned away — that is what the card says */
  .art .try-hard { opacity: 0 !important; }
  .art .turn-off { opacity: .8 !important; transform: scaleX(-1) !important; }
  .art .screen { opacity: .1 !important; }
  .art .strengthen { opacity: .9 !important; stroke-width: 2.6 !important; }
  .art .phrase { stroke-dashoffset: 0 !important; opacity: .8 !important; }
  /* the arc: the stills hold the moment each finding is legible */
  .art .rng-hold { opacity: .45 !important; }
  .art .coin { transform: none !important; }
  /* the big one is back, which is the half of the test that matters */
  .art .leaves { transform: none !important; }
  .art .back-on { opacity: .8 !important; }
  .art .tip-out { transform: rotate(18deg) !important; }
  /* the pair: the average ended up back where it started, so the still
     holds the small heart rather than the swollen one */
  .art .swell-back { transform: scale(.5) !important; opacity: .55 !important; }
  .art .let-go { opacity: .35 !important;
                 transform: translateX(calc(var(--go, 20px) / 2)) !important; }
  /* the many: the stills hold the moment after the thing has happened —
     the lens has landed, the eye is open, the second circle has arrived */
  .art .found, .art .watch, .art .clump { opacity: 1 !important;
    transform: none !important; }
  /* and this one holds short of the far side, which is the whole finding */
  .art .thr-give { stroke-dashoffset: 46 !important; opacity: .6 !important; }
  /* the rupture: every trace holds its finished shape, and the one who walked
     over has arrived — the still has to be the after, not the before */
  .art .tr-draw { stroke-dashoffset: 0 !important; opacity: .9 !important; }
  /* the disaster has finished passing: the still of this part has to be the after */
  .art .wv-sweep { stroke-dashoffset: 0 !important; opacity: .5 !important; }
  .art .wv-sweep.wv-aimed { opacity: .68 !important; }
  .art .join-in { transform: translateX(62px) !important; }
  .art .join-in-l { transform: translateX(-62px) !important; }
  /* the repair: it has already been handed over, which is the point of the card */
  .art .pass-over { transform: translateX(13px) !important; opacity: 1 !important; }
  /* the long thread: the big house has finished growing */
  .art .hut-big { transform: none !important; opacity: .95 !important; }
  /* the check: the return arrow has arrived, the range is drawn, and the
     unsettled mark holds off-centre — a still of this one that sat dead centre
     would read as settled, which is the opposite of what the card says */
  .art .pass-back { transform: translateX(-13px) !important; opacity: 1 !important; }
  /* The two plates hold at their *starts*, which pushes them apart. Holding them
     at their ends was the first attempt and it slid them into each other: the pair
     sits 18 units apart to begin with, so two 11-unit moves inward merged them
     into one figure-eight and the still said "one plate" instead of "one each way". */
  .art .pass-r { transform: translateX(-11px) !important; opacity: 1 !important; }
  .art .pass-l { transform: translateX(11px) !important; opacity: 1 !important; }
  .art .undecided { transform: translateX(7px) !important; }
  .art .rng-range { opacity: .3 !important; transform: none !important; }
  /* the night: the rota cannot rotate, so exactly one of the bright overlays is
     held on and the rest are hidden. A still with all of them lit would say
     "eight people", which is not what the card counted. */
  .art .watch-turn { opacity: 0 !important; }
  .art .watch-turn.watch-held { opacity: 1 !important; }
}

/* On the static section pages the cards sit in a document rather than a fixed
   feed, so the row can afford a little more room. */
.rl-flat .rc-art { height: clamp(64px, 11vw, 96px); }
