/* ═══════════════════════════════════════════════════════════════════════
   DEVMARK LIBRARY — the shelf: procedurally generated 3D book spines
   ═══════════════════════════════════════════════════════════════════════

   WHY THIS EXISTS
   The reference builds its shelf by squeezing each publisher's cover artwork
   into a ~30px-wide strip. The result looks like a shelf from three metres
   away and is illegible from one: the "spine text" is a compressed cover, so
   no title can actually be read. It also hotlinks artwork whose licence is
   not established, and it caps out at about fifty books before the single
   row stops working.

   Devmark's shelf is drawn, not photographed. Every spine is CSS and live
   text:

     · the title is real text in `writing-mode: vertical-rl`, so it is
       readable, selectable, searchable, translatable and reaches a screen
       reader in the right order;
     · thickness is proportional to the verified page count — the one
       dimension we actually have data for;
     · colour comes from the shelf's hue family, so a shelf reads as a chord;
     · nothing is downloaded, so a thousand spines cost no image bandwidth
       and carry no image licensing question at all.

   Height varies by a deterministic per-book hash. That is decorative texture
   only — we hold no data on physical trim size, and the interface never
   presents spine height as information.

   THE INTERACTION
   Not "the card floats up". The real gesture for getting a book off a shelf
   is a finger on the head of the spine, tipping it out. So the spine rotates
   about its foot (`transform-origin: 50% 100%`) under perspective, rises
   slightly, and the front board edge comes into view at the left. It reads
   as a physical act rather than a hover state.
   ═══════════════════════════════════════════════════════════════════════ */

/* The bookcase itself. Books standing on a plank with the page showing
   through behind them read as cut-out shapes; a real shelf has a back panel,
   and the panel is darker than the room because it is in shadow. This is what
   makes the plank look like part of a case rather than a line under a row. */
.shelf,
.shelfstage {
  background:
    linear-gradient(180deg,
      color-mix(in oklab, var(--sunk) 62%, transparent) 0%,
      var(--sunk) 14%,
      var(--sunk) 88%,
      color-mix(in oklab, var(--sunk) 70%, transparent) 100%);
  border-radius: var(--r-3);
}

/* Ambient occlusion where the back panel meets the sides, and the warm
   light falling in from the upper left. */
.shelf::before,
.shelfstage::before {
  content: "";
  position: absolute; inset: 0;
  /* Behind the books, in front of the panel. Without the negative index this
     lit-wall layer paints over the shelf and washes every book out. */
  z-index: -1;
  pointer-events: none;
  border-radius: inherit;
  background:
    radial-gradient(120% 62% at 12% 0%,
      rgba(255, 244, 224, .20), transparent 64%),
    linear-gradient(90deg,
      rgba(74, 48, 24, .14) 0%, transparent 6%,
      transparent 94%, rgba(74, 48, 24, .14) 100%);
}
.shelf, .shelfstage { position: relative; isolation: isolate; }

.shelf {
  position: relative;
  /* No shared vanishing point. A single perspective across a row this wide
     keystones anything rotated about Y: a book near the edge is far from the
     origin, so opening its cover stretched the board to twice its height.
     Each book now carries its own perspective and opens about itself, which
     is also what actually happens when someone pulls one book off a shelf. */
}

.shelf__row {
  display: flex;
  align-items: flex-end;
  gap: 3px;
  min-height: 268px;
  padding-inline: 2px;
  transform-style: preserve-3d;
}

/* The plank. Oak seen very slightly from above: a lit top edge where the
   light lands, the board face, then a darker front lip in shadow. The books
   stand ON the top edge, so that edge is the line everything aligns to. */
.shelf__plank {
  position: relative;
  height: 16px;
  margin-top: -1px;
  border-radius: 0 0 2px 2px;
  background:
    linear-gradient(180deg,
      var(--oak-lit)  0 1px,
      var(--oak)      1px 6px,
      var(--oak-deep) 6px 12px,
      var(--oak-edge) 12px 100%);
  box-shadow:
    inset 0 1px 0 rgba(255, 238, 214, .45),
    0 10px 18px -8px rgba(60, 38, 18, .40);
}

/* Grain. Two very low-contrast repeating gradients at different periods, so
   the run of a plank is not perfectly uniform without ever resolving into a
   pattern the eye can count. */
.shelf__plank::before {
  content: "";
  position: absolute; inset: 1px 0 0;
  opacity: .5;
  background:
    repeating-linear-gradient(90deg,
      rgba(92, 62, 34, .10) 0 2px, transparent 2px 37px),
    repeating-linear-gradient(90deg,
      rgba(255, 236, 208, .10) 0 1px, transparent 1px 89px);
  pointer-events: none;
}

/* The shadow the plank throws down the wall beneath it. */
.shelf__plank::after {
  content: "";
  position: absolute; inset: auto 0 -22px;
  height: 22px;
  background: linear-gradient(180deg, rgba(60, 38, 18, .22), transparent 82%);
  pointer-events: none;
}

/* Scroll-driven depth. --p is written to the row by motion.js as the row
   crosses the viewport; --i is the book's index along it. The sine term
   phases the offset so the row lifts as a wave rather than as a block, and
   the amplitude stays under 7px so this reads as parallax and never as
   something moving on its own. */
.shelf__run .spine,
.shelf__row .spine {
  --phase: calc(sin(var(--i, 0) * 0.55rad) * 1);
  transform: translateY(calc((0.5 - var(--p, 0.5)) * var(--phase) * 6px));
  transition: transform var(--d-fast) linear;
}

@media (prefers-reduced-motion: reduce) {
  .shelf__run .spine,
  .shelf__row .spine { transform: none; }
}

/* ── A single spine ────────────────────────────────────────────────────── */
.spine {
  /* Each book is its own 3D scene. 820px is close enough to read as a real
     lens without the exaggeration that a shorter distance produces. */
  perspective: 820px;
  perspective-origin: 50% 64%;
  /* Set per book by the build:
       --spine-w  thickness in px, from verified page count
       --spine-h  height in px, decorative jitter
       --hue      shelf hue family
       --lt       lightness  */
  position: relative;
  /* The build writes --spine-w and --spine-h as absolute pixels, because
     thickness is derived from the verified page count and must stay
     proportional between books. --spine-scale keeps that relationship intact
     while fitting the whole shelf to the viewport: every book shrinks by the
     same factor, so a 700-page volume is still visibly thicker than a 200-page
     one on a phone. */
  --spine-scale: 1;
  flex: 0 0 calc(var(--spine-w, 34px) * var(--spine-scale));
  width: calc(var(--spine-w, 34px) * var(--spine-scale));
  height: calc(var(--spine-h, 210px) * var(--spine-scale));
  transform-style: preserve-3d;
}

/* Laptop down: the shelf keeps its density rather than dropping books. */
@media (max-width: 1100px) { .spine { --spine-scale: .92; } }
@media (max-width: 860px)  { .spine { --spine-scale: .84; } }
@media (max-width: 620px)  { .spine { --spine-scale: .74; } }
@media (max-width: 420px)  { .spine { --spine-scale: .66; } }

.spine__a {
  position: absolute;
  inset: 0;
  display: block;
  transform-origin: 50% 100%;
  transform-style: preserve-3d;
  transition:
    transform var(--d-content) var(--e-spring-soft),
    filter var(--d-ui) var(--e-out);
  will-change: transform;
}

.spine__face {
  position: absolute;
  inset: 0;
  border-radius: 1px 1px 0 0;
  overflow: hidden;
  display: flex;
  flex-direction: column;
  align-items: center;
  /* The cloth binding. This is the base material, and it is what shows for
     the two books with no cover art; where a cover exists, .spine__art sits
     on top of it and the gradients below roll over both. */
  background: oklch(var(--lt, 38%) var(--spine-c) var(--hue, 30));
  box-shadow:
    inset 0 1px 0 rgba(255, 246, 232, .22),
    0 1px 3px rgba(40, 24, 10, .45);
}

/* The book's own cover, cropped to a vertical slice.

   object-fit: cover into a 34x225 box crops a narrow band from the middle of
   the artwork, which carries the cover's dominant colour and usually some of
   its type. An earlier version pinned the crop to the extreme left edge and
   magnified it, on the theory that a wrapped paperback's spine art comes from
   there; in practice the left edge of most covers is empty margin, and 281
   books all rendered as the same brown sliver. The middle band is what
   actually distinguishes one book from another at this width. */
.spine__art {
  position: absolute;
  inset: 0;
  width: 100%; height: 100%;
  object-fit: cover;
  object-position: 50% 46%;
  filter: saturate(1.10) contrast(1.06);
  /* Visible by default — see cards.css. The cloth binding sits behind it, so
     the image simply paints over it when it decodes. */
}
.spine__art[data-cover-failed] { display: none; }

/* The cylinder. Laid over whatever the material turned out to be, so a
   cover-clad spine and a cloth one catch the light identically. */
.spine__face::before,
.spine__face::after { z-index: 3; }
.spine__face {
  background-image: none;
}
.spine__a::before {
  content: "";
  position: absolute; inset: 0;
  z-index: 2;
  border-radius: 1px 1px 0 0;
  pointer-events: none;
  background:
    linear-gradient(90deg,
      rgba(30, 18, 8, .34) 0%,
      rgba(255, 246, 232, .05) 16%,
      rgba(255, 246, 232, .07) 42%,
      rgba(255, 246, 232, .00) 68%,
      rgba(30, 18, 8, .13) 87%,
      rgba(30, 18, 8, .38) 100%),
    linear-gradient(180deg,
      rgba(255, 246, 232, .07) 0%,
      transparent 12%,
      transparent 86%,
      rgba(30, 18, 8, .22) 100%);
}

/* Head and tail bands — the cloth detail that makes a run of spines read as
   bound books rather than coloured rectangles. */
.spine__face::before,
.spine__face::after {
  content: "";
  position: absolute; left: 0; right: 0;
  height: 3px;
  background: rgba(0,0,0,.34);
  box-shadow: 0 1px 0 rgba(255,255,255,.07);
}
.spine__face::before { top: 13px; }
.spine__face::after  { bottom: 20px; }

/* ── Spine text ──────────────────────────────────────────────────────────
   vertical-rl reads top-to-bottom, which is the correct orientation for
   English-language spines. Real text, so it is selectable and accessible. */
/* A scrim behind the spine lettering.

   With real cover art underneath, a spine is a 90-degree crop of somebody
   else's typography, and white text laid over it is unreadable on perhaps one
   book in five. A real shelf solves this because a real spine is typeset for
   reading edge-on; a cover crop is not. Legibility wins over realism here:
   the title is the only thing that identifies a book at this width, so it
   gets a wash dark enough to always carry it.

   An earlier attempt used a ::before at z-index -1, which painted behind the
   face's own background instead of just behind the text. The wash belongs on
   the text element itself. */
.spine__face:has(.spine__art:not([data-cover-failed])) .spine__title {
  background: linear-gradient(90deg,
    rgba(16, 10, 4, .18) 0%,
    rgba(16, 10, 4, .62) 26%,
    rgba(16, 10, 4, .68) 50%,
    rgba(16, 10, 4, .62) 74%,
    rgba(16, 10, 4, .18) 100%);
  backdrop-filter: saturate(.7);
}
.spine__face:has(.spine__art:not([data-cover-failed])) .spine__author {
  text-shadow: 0 1px 3px rgba(10, 6, 2, .95), 0 0 6px rgba(10, 6, 2, .7);
}

.spine__title {
  writing-mode: vertical-rl;
  position: absolute;
  top: 24px; bottom: 30px; left: 0; right: 0;
  display: flex; align-items: center; justify-content: center;
  font-family: var(--font-display);
  font-weight: 700;
  font-size: clamp(9px, calc(var(--spine-w, 34px) * 0.30), 13px);
  line-height: 1.05;
  letter-spacing: 0.005em;
  color: rgba(255,255,255,.93);
  text-shadow: 0 1px 1px rgba(0,0,0,.45);
  padding-block: 4px;
  overflow: hidden;
  text-align: center;
  /* Long titles are clipped rather than wrapped — a real spine does the
     same thing, and the full title is always one hover or one tap away. */
  white-space: nowrap;
  text-overflow: ellipsis;
}

.spine__author {
  writing-mode: vertical-rl;
  position: absolute;
  bottom: 4px; left: 0; right: 0;
  height: 14px;
  display: flex; align-items: center; justify-content: center;
  font-family: var(--font-ui);
  font-size: 7px; font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: rgba(255,255,255,.52);
  white-space: nowrap; overflow: hidden;
}

/* The front board, seen edge-on at rest. As the book tips out it catches the
   light and reads as the cover coming forward. */
.spine__board {
  position: absolute;
  top: 0; bottom: 0; left: 0;
  width: 3px;
  background: linear-gradient(90deg, rgba(255,255,255,.22), rgba(255,255,255,0));
  opacity: 0;
  transition: opacity var(--d-ui) var(--e-out);
  pointer-events: none;
}

/* ── The front board ─────────────────────────────────────────────────────
   The full cover, standing on the same foot as the spine and hinged at the
   spine's left edge. At rest it is folded back flat against the shelf and
   invisible; pulling the book open swings it forward into view. This is the
   same hinge a real book has, which is why it needs no easing trickery to
   read as physical.

   It is sized in spine widths so a thick book opens to a proportionally
   wider board, and capped so a 700-page volume does not cover its
   neighbours entirely. */
.spine__front {
  position: absolute;
  left: 0; bottom: 0;
  width: min(
    clamp(104px, calc(var(--spine-w, 34px) * 3.4), 150px) * var(--spine-scale, 1),
    42vw);
  height: calc(var(--spine-h, 210px) * 0.97 * var(--spine-scale, 1));
  transform-origin: 0% 50%;
  transform: rotateY(-88deg);
  transform-style: preserve-3d;
  backface-visibility: hidden;
  opacity: 0;
  pointer-events: none;
  border-radius: 1px 3px 3px 1px;
  overflow: hidden;
  background: var(--band);
  box-shadow: var(--lift-3);
  transition:
    transform var(--d-content) var(--e-spring-soft),
    opacity var(--d-fast) var(--e-out);
}
.spine__front-img {
  width: 100%; height: 100%;
  object-fit: cover;
  /* The BOARD animates open; the cover printed on it does not fade
     separately. Animation moves an already-resolved cover, it never decides
     whether one is shown. */
}
.spine__front-img[data-cover-failed] { display: none; }

/* A cover is printed, so it catches light across its face rather than
   sitting flat. */
.spine__front::after {
  content: "";
  position: absolute; inset: 0;
  background: linear-gradient(104deg,
    rgba(255, 246, 232, .26) 0%,
    rgba(255, 246, 232, .05) 26%,
    transparent 52%,
    rgba(30, 18, 8, .12) 100%);
  pointer-events: none;
}

/* ── Tipping out ─────────────────────────────────────────────────────────
   Hover and keyboard focus produce exactly the same motion, so the shelf is
   fully usable without a pointer.

   The book rises off the plank, tips toward the reader about its foot, and
   swings its front board open. Three things moving together on one spring
   is what separates this from a card that scales up. */
.spine__a:hover,
.spine__a:focus-visible {
  transform: translateY(-16px) translateZ(52px) rotateX(-9deg) rotateY(15deg);
  z-index: 6;
}
.spine__a:hover .spine__board,
.spine__a:focus-visible .spine__board { opacity: 1; }
.spine__a:hover .spine__front,
.spine__a:focus-visible .spine__front {
  transform: rotateY(-27deg);
  opacity: 1;
}
/* The spine's own face dims as the board opens, so attention moves to the
   cover rather than the two competing for it. */
.spine__a:hover .spine__title,
.spine__a:focus-visible .spine__title,
.spine__a:hover .spine__author,
.spine__a:focus-visible .spine__author { opacity: .25; }

@media (hover: hover) and (pointer: fine) {
  /* Neighbours lean aside to make room. Purely CSS, no JS. */
  .spine:has(+ .spine .spine__a:hover) .spine__a { transform: rotateZ(-1.4deg) translateX(-2px); }
  .spine:hover + .spine .spine__a { transform: rotateZ(1.4deg) translateX(2px); }
}

.spine__a:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
  border-radius: 2px;
}

/* Touch: there is no hover, so the first press opens the board and the
   second follows the link. The whole shelf is still reachable by tapping,
   and nothing depends on a pointer that a phone does not have. */
@media (hover: none) {
  .spine__a:active { transform: translateY(-8px) rotateX(-7deg); }
  .spine[data-peek="true"] .spine__a {
    transform: translateY(-16px) translateZ(52px) rotateX(-9deg) rotateY(15deg);
    z-index: 6;
  }
  .spine[data-peek="true"] .spine__front { transform: rotateY(-27deg); opacity: 1; }
  .spine[data-peek="true"] .spine__title,
  .spine[data-peek="true"] .spine__author { opacity: .25; }
}

/* ── Shelf peek card ─────────────────────────────────────────────────────
   The reference shows a metadata card per spine and never removes them, so
   they pile up across the screen. There is exactly one of these, it is
   positioned by JS, and it is `inert` whenever it is not shown. */
.peek {
  position: fixed;
  z-index: var(--z-overlay);
  width: 264px;
  padding: var(--s-4) var(--s-4) var(--s-4);
  background: var(--ink-800);
  border: 1px solid var(--hair-2);
  border-radius: var(--r-3);
  box-shadow: var(--lift-3);
  pointer-events: none;
  opacity: 0;
  transform: translate3d(var(--x, 0), var(--y, 0), 0) scale(0.96);
  transition:
    opacity var(--d-fast) var(--e-out),
    transform var(--d-ui) var(--e-spring-snap);
  will-change: transform, opacity;
}
.peek[data-open="true"] {
  opacity: 1;
  transform: translate3d(var(--x, 0), var(--y, 0), 0) scale(1);
}
.peek__title {
  font-family: var(--font-display); font-weight: 700;
  font-size: 1.02rem; line-height: 1.22; color: var(--paper);
  margin-bottom: 5px;
}
.peek__author { font-size: var(--t-small); color: var(--paper-2); margin-bottom: var(--s-3); }
.peek__row {
  display: flex; flex-wrap: wrap; gap: 6px 10px;
  font-size: var(--t-micro); letter-spacing: 0.06em; text-transform: uppercase;
  color: var(--paper-3);
}
.peek__swatch {
  width: 100%; height: 3px; border-radius: 2px; margin-bottom: var(--s-3);
  background: oklch(var(--lt, 28%) var(--spine-c) var(--hue, 30));
}
@media (hover: none), (prefers-reduced-motion: reduce) { .peek { display: none; } }

/* ── Horizontal rails ────────────────────────────────────────────────────
   Used where a shelf is a browsing strip rather than the whole library.
   Scroll-snap so a flick lands cleanly; overflow is contained here and
   nowhere else, which is what keeps the page from scrolling sideways. */
.rail {
  min-width: 0;
  display: flex;
  gap: var(--s-4);
  overflow-x: auto;
  overscroll-behavior-x: contain;
  scroll-snap-type: x proximity;
  padding-block: var(--s-2) var(--s-5);
  /* Bleed to the viewport edge so the row reads as continuing off-screen. */
  margin-inline: calc(var(--gutter) * -1);
  padding-inline: var(--gutter);
}
.rail > * { scroll-snap-align: start; flex: none; }

/* A shelf laid out as a wrapping run of spines, for the full library view. */
.shelf__run {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: 3px var(--s-1);
  /* Room beneath each wrapped row for its plank and the shadow it throws. */
  row-gap: calc(var(--s-6) + 26px);
}

/* The library view wraps its spines, so the rows are flex lines rather than
   elements and there is nothing to hang a plank on. Every spine therefore
   carries its own slice of plank; because a flex line aligns its items to a
   shared bottom edge, the slices meet and read as one continuous board.
   The slice is drawn wider than the spine and to the left so it closes the
   gap to its neighbour without a seam. */
.shelf__run .spine::after {
  content: "";
  position: absolute;
  left: -5px; right: -5px;
  bottom: -15px;
  height: 15px;
  z-index: 0;
  background:
    linear-gradient(180deg,
      var(--oak-lit)  0 1px,
      var(--oak)      1px 5px,
      var(--oak-deep) 5px 11px,
      var(--oak-edge) 11px 100%);
  box-shadow: 0 9px 16px -8px rgba(60, 38, 18, .42);
  pointer-events: none;
}

/* Contact shadow where the book meets the board. */
.shelf__run .spine::before {
  content: "";
  position: absolute;
  left: -3px; right: -3px; bottom: -3px;
  height: 10px;
  z-index: 1;
  background: radial-gradient(70% 100% at 50% 100%,
    rgba(40, 24, 10, .55), transparent 72%);
  pointer-events: none;
}
