/* ------------------------------------------------------------------
   The shelf. Homepage only.

   Seven volumes standing on one continuous shelf. Each is a section of
   the site, so the shelf IS the navigation rather than a decoration
   sitting above it.

   Modelled on the Complete Shelf experience by Mint (MIT licensed,
   github.com/mintdotgg/mint-playground). That original is a Three.js
   scene; this is a reimplementation in CSS 3D, because the scene is
   boxes yawing about a vertical axis inside a group that slides
   sideways, with no lighting model and no raycasting needed. DOM
   elements hit-test themselves. What is carried over is the geometry
   and the motion design; none of its code is.

   The rules that matter, taken from that design:

   - Slots never move. The ROW moves. Every book keeps a stable position
     on a continuous shelf, and selecting one slides the whole row so
     that book arrives at the reading position.
   - A book is never removed from the shelf. It is presented: drawn
     forward along Z, then turned from spine-out to cover-out.
   - The handoff is sequential. The outgoing book retreats, turns back
     to spine-out and shelves BEFORE the incoming one extracts, turns
     and settles. They never cross.
   - Books have individual heights and thicknesses. That variation is
     most of why a row reads as a library.

   Palette is this site's, not the reference's: white paper, navy
   bindings. Timings live in shelf.js, which owns the animation.
   ------------------------------------------------------------------ */

/* --- Fallback: a ruled index ------------------------------------------ */

.shelf-band { padding: clamp(2.5rem, 5vw, 4rem) 0 clamp(3rem, 6vw, 4.5rem); }

.shelf-frame,
.shelf-reader,
.shelf-arrow,
.shelf-nav-hint,
.bookend,
.shelf-board,
.shelf-lip,
.face-back,
.pages { display: none; }

/* The shelf section is full-bleed by design, so it carries no .wrap. That is
   right for the scene and wrong for the fallback, which was rendering hard
   against the viewport edge while every other block on the page sat on the
   container grid. These mirror .wrap so the index lines up with the header
   and the prose below it.

   The rule moves from the list to the first row: on the padded list it would
   have run the full width of the box, past the text it is meant to head. */
.shelf-index {
  list-style: none;
  margin: 0 auto;
  padding: 0 clamp(1.25rem, 4vw, 3.5rem);
  max-width: min(94vw, 88rem);
}

.shelf-index .volume { border-bottom: 1px solid var(--rule-2); }
/* Scoped to the no-script path explicitly. Unscoped it outweighs
   `.js-shelf .volume { border: 0 }` on specificity and paints a hairline
   across the first book of the shelf. */
/* Keyed off the bookend rather than :first-of-type. Both the bookends and
   the volumes are <li>, so :first-of-type matches the opening bookend and
   the rule never reached a book at all. The bookend is display:none here,
   but it is still in the DOM, so the sibling combinator holds. */
html:not(.js-shelf) .shelf-index .bookend-start + .volume {
  border-top: 1px solid var(--rule);
}

.shelf-index .book {
  display: flex;
  gap: 1.15rem;
  align-items: baseline;
  padding: 1.25rem 0;
  text-decoration: none;
  color: inherit;
}

/* The spine repeats the cover's words, so in the fallback only one of them
   speaks. The spine is the one that goes: it exists to be read edge-on and
   reads as a stray fragment when it is not. */
.shelf-index .face-spine,
.shelf-index .cover-imprint,
.shelf-index .cover-rule,
.shelf-index .cover-foot { display: none; }

.shelf-index .cover-title {
  display: block;
  font-family: var(--sans);
  font-size: 1.12rem;
  font-weight: 640;
  color: var(--ink);
}
.shelf-index .book:hover .cover-title { color: var(--accent); }

.shelf-index .cover-sub {
  display: block;
  margin-top: 0.25rem;
  font-family: var(--sans);
  font-size: 0.95rem;
  line-height: 1.5;
  color: var(--ink-3);
}

.shelf-index .volume::before {
  content: '';
  display: block;
  float: left;
  width: 3px;
  height: 2.4rem;
  margin: 1.35rem 1rem 0 0;
  background: var(--vol);
  border: 1px solid var(--rule-2);
}

/* ======================================================================
   The shelf
   ====================================================================== */

.js-shelf .shelf-band {
  /* The base book, before each volume's own multipliers. --bw is the cover
     width, --bh the height, --bt the thickness (which is also the slot
     pitch, since books sit flush against each other). */
  --bw: clamp(190px, 19vw, 310px);
  --bh: clamp(250px, 26vw, 430px);
  --bt: clamp(30px, 3.1vw, 52px);

  /* Where the presented book comes to rest, across the stage. Well right
     of centre: the reading panel takes the left third, exactly as the
     reference offsets its camera to clear its details panel. */
  --anchor: 70%;
  /* Where an inspected volume comes to rest. Further left than the browse
     anchor but not centred: the card sits in the left third and the book
     should fill what is left of the stage without standing on top of it. */
  --focus-anchor: 66%;
  --focus-scale: 1.2;
  --focus-lift: 0px;
  --baseline: 8.5rem;
  /* How much of the presented volume's leftward slide to apply. See
     slideFor() in shelf.js; the mobile pose sets this to 0. */
  --slide: 1;

  position: relative;
  padding: 0;
  border-top: 0;
  /* The row runs off both edges on purpose. A shelf you can see the end of
     is a short shelf. */
  overflow: hidden;
  user-select: none;
}

.js-shelf .shelf-stage {
  position: relative;
  /* svh, not vh: mobile browsers count their collapsing toolbar in vh, and
     the shelf would sit under it on first paint. */
  height: clamp(30rem, calc(100svh - 5rem), 44rem);
  perspective: 1900px;
  /* The eye sits just above the shelf line, not at the middle of the stage.

     Perspective moves a point away from the origin as it comes toward the
     camera, so with the origin up at mid-stage the feet of a presented book
     swung down well below the shelf and the front edge of the board sliced
     the cover in half. Put the origin at the baseline and the feet stay
     put: a book drawn forward grows upward off the shelf it is standing on,
     which is what happens when you pull one toward you. */
  perspective-origin: var(--anchor) calc(100% - var(--baseline) - 1rem);
  cursor: grab;
  touch-action: pan-y;
}
.js-shelf .shelf-stage.is-dragging { cursor: grabbing; }

/* Hover on a bound volume italicises the title and leaves its colour alone.
   Solomon, 30 Jul. The rule above at .shelf-index .book:hover was written for
   the ruled fallback, where the title is navy on white and turning it to the
   accent reads as a link. It was reaching the shelf too, because both use the
   same .shelf-index list, so hovering a dark volume swapped white type for
   navy on a navy board and the title all but vanished. Ink stays; the slope
   carries the state. */
.js-shelf .shelf-index .book:hover .cover-title,
.js-shelf .shelf-index .book:hover .spine-title {
  color: inherit;
  font-style: italic;
}

/* --- The row ----------------------------------------------------------- */

/* Flexbox, so each volume's own thickness accumulates into a slot position
   without anything computing a running total by hand. shelf.js reads the
   slots back off the box. */
.js-shelf .shelf-index {
  position: absolute;
  left: 0;
  bottom: var(--baseline);
  display: flex;
  align-items: flex-end;
  margin: 0;
  padding: 0;
  border: 0;
  max-width: none;
  list-style: none;
  transform-style: preserve-3d;
  /* Written by shelf.js every frame. The row moves; the slots do not. */
  transform: translate3d(var(--rail, 0px), 0, 0);
  will-change: transform;
}

.js-shelf .volume {
  position: relative;
  flex: 0 0 calc(var(--bt) * var(--tm, 1));
  height: calc(var(--bh) * var(--hm, 1));
  border: 0;
  transform-style: preserve-3d;
}
.js-shelf .volume::before { display: none; }

/* Uprights at each end. Seven volumes cannot fill a screen the way the
   reference's nineteen do, so without these the row simply stops in the
   middle of a bare board and reads as a shelf someone emptied. A bookend
   makes the end of the row a statement instead of an absence. */
.js-shelf .bookend {
  display: block;
  position: relative;
  flex: 0 0 clamp(10px, 0.9vw, 16px);
  height: calc(var(--bh) * 1.06);
  border: 0;
  background: linear-gradient(90deg, #2b3446 0%, #46516a 34%, #202838 100%);
  box-shadow: 0 12px 22px -14px rgba(16, 23, 40, 0.7);
}
.js-shelf .bookend-start { margin-right: clamp(4px, 0.4vw, 8px); }
.js-shelf .bookend-end { margin-left: clamp(4px, 0.4vw, 8px); }

/* --- The book ---------------------------------------------------------- */

/* A box centred on its slot. Yaw 90deg presents the spine, yaw 0 the
   cover, which is the reference's convention exactly. shelf.js writes
   --z, --yaw and --sc; the stylesheet only says what they mean. */
.js-shelf .book {
  position: absolute;
  left: 50%;
  bottom: 0;
  width: calc(var(--bw) * var(--hm, 1));
  height: 100%;
  margin-left: calc(var(--bw) * var(--hm, 1) / -2);
  display: block;
  padding: 0;
  text-decoration: none;
  color: inherit;
  transform-style: preserve-3d;
  transform:
    translate3d(var(--x, 0px), var(--y, 0px), var(--z, 0px))
    rotateY(var(--yaw, 90deg))
    rotateX(var(--rx, 0deg))
    scale(var(--sc, 1));
}

.js-shelf .book:focus-visible { outline: none; }
.js-shelf .volume:has(.book:focus-visible) .face-spine,
.js-shelf .volume:has(.book:focus-visible) .face-cover {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
}

/* --- Faces --------------------------------------------------------------- */

.js-shelf .face {
  position: absolute;
  top: 0;
  left: 0;
  height: 100%;
  display: block;
  backface-visibility: hidden;
  background: var(--vol);
  color: var(--vol-ink);
}

/* Front cover, on the +Z face of the box. */
.js-shelf .face-cover {
  width: 100%;
  transform: translateZ(calc(var(--bt) * var(--tm, 1) / 2));
  display: flex;
  flex-direction: column;
  padding: clamp(1.1rem, 1.7vw, 2rem);
  /* The boards are the boards. Copy that outgrows the cover used to carry on
     past the foot of the book and print down the shelf, which is what Solomon
     photographed on 30 Jul. The standfirsts now fit, so this clips nothing;
     it is here so that a later, longer line cannot put type on the timber
     again. */
  overflow: hidden;
  background-image: linear-gradient(
    115deg,
    rgba(255, 255, 255, 0.13) 0%,
    rgba(255, 255, 255, 0.03) 40%,
    rgba(0, 0, 0, 0.12) 100%
  );
}

/* Spine, on the -X face. At yaw 90deg this is what faces the reader. */
.js-shelf .face-spine {
  width: calc(var(--bt) * var(--tm, 1));
  transform: rotateY(-90deg) translateZ(calc(var(--bw) * var(--hm, 1) / 2));
  background-image: linear-gradient(
    90deg,
    rgba(0, 0, 0, 0.30) 0%,
    rgba(255, 255, 255, 0.13) 20%,
    rgba(255, 255, 255, 0.03) 58%,
    rgba(0, 0, 0, 0.26) 100%
  );
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.16);
}

/* Back board, on the -Z face. The reference builds front and back boards from
   the same geometry; without one, a book turning past 90deg showed its hollow
   inside. Shaded down because it faces away from the light. */
.js-shelf .face-back {
  width: 100%;
  transform: translateZ(calc(var(--bt) * var(--tm, 1) / -2)) rotateY(180deg);
  background-image: linear-gradient(
    115deg,
    rgba(0, 0, 0, 0.30) 0%,
    rgba(0, 0, 0, 0.16) 60%,
    rgba(0, 0, 0, 0.34) 100%
  );
}

/* --- The text block ------------------------------------------------------ */

/* The pages, as the reference models them: a paper box a little smaller than
   the boards in every dimension, so the covers overhang it. In the reference
   that is RoundedBoxGeometry(width - 0.075, height - 0.105, depth - 0.052);
   PAGE_INSET and PAGE_TRIM below are those proportions.

   Only ever caught at an angle as a book turns, and it is the detail that
   stops the turn reading as a flipping card. */
.js-shelf .pages {
  /* Explicit: the no-script rule hides .pages, and unlike the boards this is
     not a .face, so nothing else turns it back on for the shelf. */
  display: block;
  --page-inset: 4px;   /* head and tail: how far the boards overhang */
  --page-trim: 3px;    /* fore-edge: how far the block sits behind the edge */
  position: absolute;
  top: var(--page-inset);
  bottom: var(--page-inset);
  left: 0;
  width: 100%;
  transform-style: preserve-3d;
  pointer-events: none;
}

.js-shelf .page-face {
  position: absolute;
  display: block;
  background: #efece3;
  backface-visibility: hidden;
}

/* Fore-edge, opposite the spine. Ruled to suggest individual leaves. */
.js-shelf .page-fore {
  top: 0;
  bottom: 0;
  left: 0;
  width: calc(var(--bt) * var(--tm, 1) - 2px);
  transform: rotateY(90deg)
             translateZ(calc(var(--bw) * var(--hm, 1) / 2 - var(--page-trim)));
  background-image: repeating-linear-gradient(
    90deg,
    rgba(120, 110, 92, 0.22) 0px,
    rgba(120, 110, 92, 0.22) 1px,
    transparent 1px,
    transparent 3px
  );
  box-shadow: inset 0 0 6px rgba(90, 80, 62, 0.18);
}

/* Head: the top of the text block, seen from slightly above, which is where
   the camera sits. Ruled the other way, along the leaves. */
.js-shelf .page-head {
  top: 0;
  left: 0;
  width: 100%;
  height: calc(var(--bt) * var(--tm, 1) - 2px);
  transform-origin: top center;
  transform: rotateX(90deg);
  background-image: repeating-linear-gradient(
    90deg,
    rgba(120, 110, 92, 0.14) 0px,
    rgba(120, 110, 92, 0.14) 1px,
    transparent 1px,
    transparent 4px
  );
}

/* Head and tail bands. */
.js-shelf .face-spine::before,
.js-shelf .face-spine::after {
  content: '';
  position: absolute;
  left: 14%;
  right: 14%;
  height: 2px;
  background: currentColor;
  opacity: 0.32;
}
.js-shelf .face-spine::before { top: 0.8rem; }
.js-shelf .face-spine::after { bottom: 0.8rem; }

/* --- Spine type ---------------------------------------------------------- */

/* Set along the spine, reading bottom to top, which is how English trade
   bindings run and what the reference uses. Laid out as if the spine were
   turned flat and then stood up: that keeps the flex axes ordinary, so the
   column stacks title then author ACROSS the spine's thickness. */
.js-shelf .spine-text {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  display: flex;
  align-items: center;
  justify-content: flex-start;
  writing-mode: vertical-rl;
  transform: rotate(180deg);
  padding: 1.6rem 0 2.2rem;
  gap: 0.45em;
}

.js-shelf .spine-title {
  font-family: var(--serif);
  font-size: clamp(0.9rem, 1.05vw, 1.2rem);
  font-weight: 600;
  line-height: 1;
  letter-spacing: -0.008em;
  white-space: nowrap;
}

/* --- Cover type ---------------------------------------------------------- */

/* A ruled border set in from the trim: the oldest cover device there is,
   and it does the work an illustration would otherwise have to do. This
   site carried hairline drawings once and they were cut, so nothing here
   reintroduces one. */
.js-shelf .face-cover::before {
  content: '';
  position: absolute;
  inset: clamp(0.6rem, 0.9vw, 1rem);
  border: 1px solid currentColor;
  opacity: 0.34;
  pointer-events: none;
}

.js-shelf .cover-title {
  display: block;
  margin-top: auto;
  font-family: var(--serif);
  font-size: clamp(1.2rem, 2vw, 2.1rem);
  font-weight: 600;
  line-height: 1.06;
  letter-spacing: -0.022em;
  color: inherit;
}

/* The rule inherits the auto bottom margin the standfirst used to carry. The
   cover is a flex column and the spacing was doing real work: .cover-title
   takes margin-top:auto and the standfirst took margin-bottom:auto, so the
   pair sat together with the volume number pinned to the foot. Hiding the
   standfirst without moving that margin drops the number up under the rule
   and leaves all the free space above the title. */
.js-shelf .cover-rule {
  display: block;
  width: 2rem;
  height: 1px;
  margin: 0.7rem 0 auto;
  background: currentColor;
  opacity: 0.5;
}

/* Off the boards, 8 Aug, at Solomon's instruction: a cover carries a title and
   nothing else, and six covers each running three lines of italic made the
   shelf read as a wall of blurbs rather than as books.

   Hidden here rather than removed from the markup, because the same span is
   the descriptive line of the ruled index this shelf falls back to when
   scripts are blocked (`.shelf-index .cover-sub` above). Deleting it would
   leave the fallback, and every crawler, with a list of six bare titles. It
   also still feeds data-sub, which the reader panel under the shelf reads. */
.js-shelf .cover-sub { display: none; }

.js-shelf .cover-foot {
  display: block;
  font-family: var(--sans);
  font-size: clamp(0.52rem, 0.58vw, 0.64rem);
  font-weight: 640;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  opacity: 0.85;
}

/* --- The shelf itself ----------------------------------------------------- */

/* The reference stands its books on walnut, and after a spell as pale slate
   so does this. The slate reasoning was that timber would be the only
   material on an otherwise white and navy page; in practice it read as one
   more volume lying on its side.

   The board sits behind the row and the lip in front of it, so the books
   stand on something and are cut off by it the way they would be on a real
   shelf. Both run the length of the row, which shelf.js measures. */

/* display is restated on all four of these because the fallback block above
   hides them, and a rule that only sets position and background loses to it. */
/* Dark wood. Solomon, 30 Jul. This was a pale blue-grey drawn from the same
   family as the bindings, which made the shelf read as another book lying
   down rather than as the thing the books stand on. Walnut separates the
   furniture from the volumes, and it is the one warm note on the page, so
   it is kept low in chroma rather than orange.

   The grain is a repeating gradient, not an image: two-pixel lights every
   seven pixels at very low alpha, which reads as grain at arm's length and
   costs no request. */
.js-shelf .shelf-board {
  display: block;
  position: absolute;
  left: 0;
  bottom: calc(var(--baseline) - 1.7rem);
  height: 1.7rem;
  background-image:
    repeating-linear-gradient(
      90deg,
      rgba(255, 245, 230, 0.045) 0 2px,
      rgba(0, 0, 0, 0) 2px 7px
    ),
    linear-gradient(180deg, #5a4029 0%, #43301f 45%, #2c1e13 100%);
  border-top: 1px solid #6d5137;
}

/* The front edge of the board, painted after the row so the books stand
   behind it and are cut off by it the way they are on a real shelf. It
   overlaps their feet by a fraction rather than butting against them.
   Lighter than the face below it, because this edge is the surface the
   light actually reaches. */
.js-shelf .shelf-lip {
  display: block;
  position: absolute;
  left: 0;
  bottom: calc(var(--baseline) - 0.4rem);
  height: 0.62rem;
  background-image:
    repeating-linear-gradient(
      90deg,
      rgba(255, 245, 230, 0.05) 0 2px,
      rgba(0, 0, 0, 0) 2px 7px
    ),
    linear-gradient(180deg, #6d5137 0%, #46321f 100%);
  border-bottom: 1px solid #24180f;
  pointer-events: none;
}

/* Contact shadow under the row. */
.js-shelf .shelf-board::before {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  bottom: 100%;
  height: 2rem;
  background: linear-gradient(180deg, rgba(16, 23, 40, 0) 0%, rgba(16, 23, 40, 0.15) 100%);
}

/* --- Frame: the running head ---------------------------------------------- */

.js-shelf .shelf-frame {
  display: flex;
  align-items: flex-start;
  /* flex-end, not space-between: with the rubric gone the count is the only
     child, and space-between parked it on the left. */
  justify-content: flex-end;
  gap: 1.5rem;
  position: absolute;
  top: clamp(1.2rem, 3vw, 2.2rem);
  left: 0;
  right: 0;
  z-index: 3;
  padding: 0 clamp(1.25rem, 4vw, 3.5rem);
  pointer-events: none;
}

.js-shelf .shelf-count {
  margin: 0;
  max-width: none;
  /* CLAUDE.md floor for letter-spaced uppercase: 12px at weight 600. */
  font-family: var(--sans);
  font-size: 0.75rem;
  font-weight: 600;
  line-height: 1.5;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink-3);
}
.js-shelf .shelf-count { display: block; text-align: right; white-space: nowrap; }


/* --- Reading panel --------------------------------------------------------- */

.js-shelf .shelf-reader {
  display: block;
  position: absolute;
  left: 0;
  bottom: calc(var(--baseline) + 1rem);
  z-index: 2;
  width: min(32ch, 36%);
  padding: 0 0 0 clamp(1.25rem, 4vw, 3.5rem);
  pointer-events: none;
}
.js-shelf .shelf-reader a { pointer-events: auto; }

.js-shelf .reader-index {
  display: flex;
  align-items: center;
  gap: 0.7rem;
  margin: 0 0 0.7rem;
  max-width: none;
  font-family: var(--mono);
  font-size: 0.75rem;
  font-weight: 600;
  letter-spacing: 0.08em;
  color: var(--ink-3);
}
.js-shelf .reader-index .hair {
  display: block;
  flex: 0 0 auto;
  width: 2.2rem;
  height: 1px;
  background: var(--rule);
}
.js-shelf .reader-n { color: var(--accent); }

.js-shelf .reader-title {
  margin: 0 0 0.5rem;
  max-width: none;
  font-family: var(--serif);
  font-size: clamp(2rem, 4vw, 3.4rem);
  font-weight: 600;
  line-height: 1.02;
  letter-spacing: -0.028em;
  color: var(--ink);
  text-wrap: balance;
}

/* The standfirst is printed on the cover, so the card no longer repeats it at
   any width. It was being set here and on the book at once, in two different
   wordings, which was the last of the duplications on this block. The volume
   is still announced in full by .shelf-status, which is a separate live
   region and unaffected. */
.js-shelf .reader-sub {
  display: none;
}

.js-shelf .reader-open {
  display: inline-flex;
  align-items: center;
  gap: 0.55rem;
  padding-bottom: 0.5rem;
  border-bottom: 1.5px solid var(--accent);
  font-family: var(--sans);
  font-size: 0.75rem;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink);
  text-decoration: none;
  transition: color 0.18s var(--ease), gap 0.18s var(--ease);
}
.js-shelf .reader-open:hover { color: var(--accent); gap: 0.85rem; }

/* --- The open volume ------------------------------------------------------- */

/* The panel is one element in two states. Browsing, it names the volume at
   the reading position. Inspecting, it becomes that volume's card: a way
   back at the top and the full description. The engine owns the geometry;
   this only dresses whichever state it reports.

   Interface curve is the reference's: cubic-bezier(0.22, 1, 0.36, 1), a
   quick ease-out that agrees with the focus motion. Transform and opacity
   only, never a property that forces layout. */
.js-shelf .reader-back,
.js-shelf .reader-desc {
  display: none;
}

.js-shelf .shelf-stage.is-focused .reader-back { display: inline-flex; }
.js-shelf .shelf-stage.is-focused .reader-desc { display: block; }

/* The standfirst and the description say the same thing at two lengths, so
   only one of them speaks at a time: the short one while browsing, the full
   one once the volume is open. */
.js-shelf .shelf-stage.is-focused .reader-sub { display: none; }

/* The panel takes pointer events only when it has controls in it. */
.js-shelf .shelf-stage.is-focused .shelf-reader { pointer-events: auto; }

/* Everything the open book is not. The shelf itself recedes rather than
   vanishing, so the reader can still see what they came out of. */
.js-shelf .shelf-stage.is-focused ~ .shelf-frame,
.js-shelf .shelf-band:has(.is-focused) .shelf-frame,
.js-shelf .shelf-stage.is-focused .shelf-board,
.js-shelf .shelf-stage.is-focused .shelf-lip,
/* The bookends go with the board they stand on. Left behind, they read as
   two dark bars floating over nothing, since the shelf under them is at
   zero and they are flex children of the row rather than of the board. */
.js-shelf .shelf-stage.is-focused .bookend,
/* The arrows move the shelf, which means nothing while a volume is open, and
   the line naming them goes with them. */
.js-shelf .shelf-stage.is-focused .shelf-nav-hint,
.js-shelf .shelf-stage.is-focused .shelf-arrow {
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.34s cubic-bezier(0.22, 1, 0.36, 1);
}

.js-shelf .shelf-stage.is-focused .volume:not(.is-open) {
  opacity: 0.13;
  transition: opacity 0.46s cubic-bezier(0.22, 1, 0.36, 1);
}
.js-shelf .volume { transition: opacity 0.34s cubic-bezier(0.22, 1, 0.36, 1); }

.js-shelf .reader-back {
  align-items: center;
  gap: 0.6rem;
  margin: 0 0 1.1rem;
  padding: 0.5rem 0.9rem;
  /* Opaque white, not transparent. Solomon, 30 Jul. This control has to be
     readable wherever the card happens to fall, and the card is laid over a
     3D scene: with no background of its own it was setting 12px grey type
     across a navy cover, which no choice of ink fixes. It carries its own
     paper now, and the hairline is what stops that paper from disappearing
     into the sheet behind it. */
  background: #fff;
  border: 1px solid var(--rule);
  border-radius: 2px;
  cursor: pointer;
  font-family: var(--sans);
  font-size: 0.75rem;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink-3);
  transition: color 0.18s var(--ease), border-color 0.18s var(--ease);
}
.js-shelf .reader-back:hover { color: var(--accent); border-color: var(--accent); }

.js-shelf .reader-desc {
  margin: 0 0 1.4rem;
  max-width: 42ch;
  font-family: var(--serif);
  font-size: 1rem;
  line-height: 1.55;
  color: var(--body, #3f4859);
}

/* An open volume is stationary and takes no gesture, so the stage stops
   offering one: no grab cursor, and touch-action stays at the browsing
   pan-y, which lets the page scroll normally over an opened book. */
.js-shelf .shelf-stage.is-inspecting { cursor: default; }

/* The page's back-to-top button is fixed to the bottom right at z-index 55,
   which put it on top of the card's own controls once the card went full
   width, clipping the label of whatever sat at that corner. It is also the
   wrong offer while a volume is open: the way out of inspection is the way
   back to the shelf, not the way back up the page. */
body:has(.shelf-stage.is-focused) .to-top {
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.34s cubic-bezier(0.22, 1, 0.36, 1);
}

/* --- Arrows ---------------------------------------------------------------- */

/* One on each side of the books, vertically on the shelf line, which is where
   Solomon asked for them on 30 Jul and is the arrangement that makes the
   control read as a direction rather than a pair of buttons.

   They were in the bottom right corner before, together, because centred on
   the sides they sat on top of the reading panel. That panel occupies the
   left third, so the left arrow is placed clear of it: the row is anchored at
   70% and the arrows sit just inside the stage edges, above the card rather
   than across it. */
.js-shelf .shelf-arrow {
  display: grid;
  place-items: center;
  position: absolute;
  bottom: 3.4rem;
  z-index: 4;
  width: 2.6rem;
  height: 2.6rem;
  border: 1px solid var(--rule);
  border-radius: 50%;
  background: color-mix(in srgb, var(--paper) 86%, transparent);
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
  color: var(--ink-2);
  font-size: 1rem;
  line-height: 1;
  cursor: pointer;
  transition: color 0.18s var(--ease), border-color 0.18s var(--ease),
              opacity 0.18s var(--ease);
}
.js-shelf .shelf-arrow:hover { color: var(--accent); border-color: var(--accent); }
.js-shelf .shelf-arrow:disabled { opacity: 0.28; cursor: default; }
.js-shelf .shelf-arrow:disabled:hover { color: var(--ink-2); border-color: var(--rule); }

/* Level with the middle of the books rather than the foot of the stage. The
   books stand at --baseline and are --bh tall, so their midline is one half
   of --bh above it, less half the button to centre it.

   Across the stage they are placed by shelf.js, from the same projected row
   extent that sizes the board, so they sit against the books wherever the row
   happens to be rather than out at the edges of a 1425px stage with several
   hundred pixels of white between them and the thing they move. These two
   values are only the fallback for the frame before the first paint. */
.js-shelf .shelf-prev,
.js-shelf .shelf-next {
  bottom: calc(var(--baseline) + var(--bh) / 2 - 1.6rem);
  /* Bigger and darker than the rest of the furniture: this is now the only
     control the shelf has, and at 2.6rem in hairline grey it read as a
     footnote to the scene rather than the way through it. */
  width: 3.2rem;
  height: 3.2rem;
  border-width: 1.5px;
  border-color: var(--rule);
  color: var(--ink);
  font-size: 1.2rem;
}
.js-shelf .shelf-prev:hover:not(:disabled),
.js-shelf .shelf-next:hover:not(:disabled) {
  background: var(--accent);
  border-color: var(--accent);
  color: #fff;
}
.js-shelf .shelf-prev { left: clamp(1.25rem, 4vw, 3.5rem); }
.js-shelf .shelf-next { right: clamp(1.25rem, 4vw, 3.5rem); }



/* ======================================================================
   Narrow screens
   ====================================================================== */

/* A shelf viewed edge-on needs width for the row, the turn and the reading
   panel side by side. Below this there is not enough of it, and the
   reference itself switches to "a centred, smaller pose and a slightly
   wider camera" on mobile. This goes further and drops the third
   dimension: six full-bleed spines, all in view, each a direct link.
   The books are still books, and nothing has to be learned.

   This is why a phone shows no animation and no autoplay: the 3D scene is
   switched off here, not missing. shelf.js checks the same 1024px query and
   refuses to run the carousel below it. */

/* The reference does NOT drop its scene on a phone: "Mobile uses a centered,
   smaller pose and a slightly wider camera." This shelf used to switch the
   third dimension off entirely below 1024px, which is why a phone showed a
   list of coloured bars and no motion at all. It now keeps the books and
   follows the reference instead: anchor centred rather than at 70%, smaller
   base dimensions, and a longer perspective so the whole row stays in frame
   without the near books ballooning.

   The reading panel moves under the shelf, because at 390px there is no room
   beside it. The flat list survives below 360px and, more importantly, as the
   no-JavaScript path, which is where it always earned its keep. */

@media (min-width: 360px) and (max-width: 1024px) {
  .js-shelf .shelf-band {
    /* Bigger than the first mobile pass. With the cover standfirst and the
       author imprint both gone the cover carries a title and a folio only,
       so the books can take the width they were being denied. */
    --bw: clamp(150px, 45vw, 250px);
    --bh: clamp(200px, 60vw, 340px);
    --bt: clamp(25px, 7.2vw, 42px);
    --anchor: 50%;
    /* Centred: the card is below the books here, not beside them. */
    --focus-anchor: 50%;
    /* An opened volume does not rise, and it shrinks. Solomon, 30 Jul:
       "why does the book go off the page? can we shrink it?"

       It used to lift 120px to clear the card. Then the card became a sheet
       that paints over the scene, so the lift bought nothing and cost the
       top of the book: the stage is 82svh, and on a phone showing Safari's
       toolbars that resolves to about 611px rather than the 672px a desktop
       window reports. Measured there, the focused cover overshot the top of
       the stage by 68px and the title was cut off entirely.

       Tuning the two numbers against that viewport would only move the
       failure to a shorter one. A lift of zero and a scale under one makes
       it structural instead: an opened volume is strictly smaller than the
       same volume browsing, and stands in the same place, so wherever the
       shelf fits at all the open book fits too. The change of state is
       carried by the neighbours receding and the card arriving, which is
       what the phone pose has always leaned on. */
    --focus-lift: 0px;
    --focus-scale: 0.88;

    /* The stage height, named so the books can be measured against it. */
    --stage-h: clamp(27rem, 82svh, 42rem);

    /* Where the books stand, and how much room is left below them for the
       card, the line and the arrows: about 5.8rem of card, 1.2rem of line,
       3.45rem of arrows and the gaps between them. */
    --baseline: 12.5rem;

    /* THE BOOK IS MEASURED AGAINST THE STAGE, not against the width alone.
       This is the fix for "why does the book go off the page".

       --bh was 60vw capped at 340px, which knows nothing about how tall the
       viewport is, while the stage follows svh. On a phone showing Safari's
       toolbars the stage came out around 525px while the books still wanted
       236px standing on a baseline of 19rem, which is 540px of scene in a
       525px box: the top of the cover, and its title with it, was cut off.
       Two rounds of tuning the focus scale and lift only moved the failure to
       a shorter viewport, because neither number was the one that was wrong.

       Taking the smaller of the two means a short screen gets shorter books
       rather than clipped ones, and the row cannot outgrow its stage at any
       viewport. Every volume's --hm is 1 or less, so this is the ceiling for
       all six. */
    --bh: min(
      clamp(200px, 60vw, 340px),
      calc(var(--stage-h) - var(--baseline) - 1.5rem)
    );

    /* Centred, with nothing beside the books to clear. */
    --slide: 0;
  }

  .js-shelf .shelf-stage {
    height: var(--stage-h);
    /* Wider camera: a longer focal length flattens the row so six books at
       phone width do not fan out past the edges of the screen. */
    perspective: 2800px;
  }

  /* Under the books, full width, centred. */
  .js-shelf .shelf-reader {
    left: 0;
    right: 0;
    width: auto;
    /* Up under the shelf, not adrift halfway to the foot. At 4.2rem the card
       sat 146px below the board with 12px left between its link and the
       arrows, so the volume it names read as unrelated to the books above it
       and crowded onto the controls below. It now clears the board by about
       36px, and the space it gave up sits between the card and the controls,
       where a gap means something. */
    bottom: 5.8rem;
    padding: 0 clamp(1.25rem, 5vw, 2rem);
    text-align: center;
  }
  /* Open, the card becomes a sheet of the same paper the page is printed on,
     divided from the scene by a hairline and laid over it. Not a card and not
     a tinted band: the background is the page's own white and the only added
     chrome is the rule, which is the treatment the rest of the site uses.

     This is what makes the state safe at any height. Browsing, the card is
     two short lines and floats clear of the books. Open, it carries the way
     back and the full description, and on a short phone there is simply not
     enough stage for both. Before this the card kept its
     transparent background and grew upward into the books, printing "RETURN
     TO THE SHELF", the one control that leaves inspection, in 12px grey over
     a mid-blue cover. Now the sheet paints over whatever it reaches and the
     book stands behind it. */
  .js-shelf .shelf-stage.is-focused .shelf-reader {
    bottom: 0;
    /* Above the row. The books sit in the stage's 3D context and were
       painting over the card's top edge. */
    z-index: 6;
    padding-top: 1.15rem;
    padding-bottom: 1.5rem;
    background: #fff;
    border-top: 1px solid var(--rule);
  }

  /* The folio says "03 of 06" and the rubric above the shelf already says
     there are six. On a phone that line is pure height. */
  .js-shelf .reader-index { display: none; }
  .js-shelf .reader-title {
    font-size: clamp(1.4rem, 6.4vw, 1.9rem);
    margin-bottom: 0.15rem;
  }
  .js-shelf .reader-sub {
    margin-left: auto;
    margin-right: auto;
    font-size: 0.9rem;
  }

  /* The standfirst is off the cover at every width, Solomon 8 Aug, so the
     phone override that used to put it back is gone.

     History, because this has moved twice: it was hidden at this width and
     printed in the reading card instead, on the grounds that a phone-width
     cover could not hold it; on 30 Jul it went back on the book and the card
     stopped repeating it. It now comes off the book as well. A cover carries
     its title and its folio, and that is all.

     Nothing replaces it below the shelf: .reader-sub is display:none in the
     panel, so the line is simply gone from the scene rather than moved. */
  .js-shelf .cover-title { font-size: clamp(1.1rem, 5.2vw, 1.55rem); }
  .js-shelf .face-cover { padding: 1rem 0.95rem; }


  /* The arrows come to the foot and pair up, because at 393px the stage is
     narrower than the row and arrows pinned to its edges stand on the books:
     the right one was printing across the Perspectives spine. Centred under
     the card they read as a pager, they are clear of the page's fixed
     back-to-top button in the corner, and they are the only control the
     shelf has left now that the slider and the autoplay are gone.

     2.75rem, not the desktop 2.6rem: 44px is the floor for a thumb. */
  .js-shelf .shelf-prev,
  .js-shelf .shelf-next {
    bottom: 0.7rem;
    width: 2.75rem;
    height: 2.75rem;
  }
  .js-shelf .shelf-prev { left: 50%; right: auto; margin-left: -3.15rem; }
  .js-shelf .shelf-next { left: 50%; right: auto; margin-left: 0.4rem; }

  /* The line above them, naming both gestures. */
  .js-shelf .shelf-nav-hint {
    display: block;
    position: absolute;
    left: 0;
    right: 0;
    bottom: 4.3rem;
    margin: 0;
    max-width: none;
    text-align: center;
    font-family: var(--sans);
    /* CLAUDE.md floor for letter-spaced uppercase: 12px at weight 600. */
    font-size: 0.75rem;
    font-weight: 600;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--muted, #4b5468);
  }

  /* The running head is desktop furniture and the card already sits here. */
  .js-shelf .shelf-count { display: none; }
}

/* --- The flat fallback ---------------------------------------------------- */

/* Below 360px, and for anyone without JavaScript at any width. */
@media (max-width: 359px) {
  .js-shelf .shelf-band { overflow: visible; }

  .js-shelf .shelf-stage {
    height: auto;
    perspective: none;
    cursor: auto;
    touch-action: auto;
    padding: 0 0 1.5rem;
  }

  .js-shelf .shelf-board,
  .js-shelf .shelf-lip,
  .js-shelf .bookend,
  .js-shelf .shelf-arrow,
  .js-shelf .shelf-reader,
  .js-shelf .face-cover,
  .js-shelf .face-back,
  .js-shelf .pages { display: none; }

  .js-shelf .shelf-frame {
    position: static;
    padding: 0 clamp(1.25rem, 4vw, 3.5rem) 1.3rem;
  }
  .js-shelf .shelf-count { display: none; }

  .js-shelf .shelf-index {
    position: static;
    display: block;
    transform: none;
    transform-style: flat;
  }

  .js-shelf .volume {
    position: static;
    height: clamp(58px, 15vw, 92px);
    transform-style: flat;
  }

  .js-shelf .book {
    position: relative;
    left: auto;
    bottom: auto;
    width: auto;
    /* Height as well as position. Off the shelf the volume is back in
       normal flow, and without this the book collapses to nothing and
       takes the spine's background with it, leaving the dark-bound
       volumes set in white ink on white paper. */
    display: block;
    height: 100%;
    margin-left: 0;
    transform: none;
    transform-style: flat;
  }

  .js-shelf .face-spine {
    position: absolute;
    inset: 0;
    width: auto;
    transform: none;
    display: flex;
    align-items: center;
    padding: 0 clamp(1.25rem, 4vw, 3.5rem);
  }

  /* Back to ordinary horizontal setting: a phone-width spine is wide and
     shallow, which is exactly the shape a normal line of type wants. */
  .js-shelf .spine-text {
    position: static;
    writing-mode: horizontal-tb;
    transform: none;
    padding: 0;
    justify-content: space-between;
    gap: 1.5rem;
  }
  .js-shelf .spine-title { font-size: clamp(1.1rem, 4.2vw, 1.6rem); }
}

/* --- Print ------------------------------------------------------------------ */

/* A 3D scene prints as a smear. On paper the shelf reverts to the index it
   is built on, which is the form that survives being printed. */
@media print {
  .js-shelf .shelf-band { overflow: visible; }
  .js-shelf .shelf-stage { height: auto; perspective: none; }
  .js-shelf .shelf-frame,
  .js-shelf .shelf-reader,
  .js-shelf .shelf-arrow,
  .js-shelf .shelf-board,
  .js-shelf .shelf-lip,
  .js-shelf .bookend,
  .js-shelf .face-spine,
  .js-shelf .face-back,
  .js-shelf .pages { display: none; }
  .js-shelf .shelf-index {
    position: static; display: block; transform: none;
    border-top: 1px solid #ccc;
  }
  .js-shelf .volume {
    position: static; height: auto; transform: none;
    border-bottom: 1px solid #ccc;
  }
  .js-shelf .book {
    position: static; width: auto; height: auto; margin-left: 0; transform: none;
  }
  .js-shelf .face-cover {
    position: static; width: auto; height: auto; transform: none;
    display: block; padding: 0.6rem 0; background: none; color: #000;
  }
  .js-shelf .face-cover::before { display: none; }
}
