/* ═══════════════════════════════════════════════════════════════════════════════════════════
   THE SHELF — landscape browse view for the reader.

   Built to DESIGN.md's Craft Bible, not to a mood board:
     · the reader is the OATS reading-desk (cork is forbidden here)
     · flat solid ink offsets — never a blurred or gradient shadow
     · torn plates via #roughsm, and the filter rides the PLATE, never the content
     · scrap-label tape for anything that has to carry text over artwork
     · Happy #F1DD01 is the reserved live-AND-present beacon and appears nowhere else
     · tilts are systematic (odd/even), 2–3°, because imperfection signals a human hand

   Loaded by demos/book-reader/reader.html. Everything is namespaced under #shelfview or .sv-
   so it cannot collide with the pile view's styles in that file.
   ═══════════════════════════════════════════════════════════════════════════════════════════ */

#shelfview{
  position:fixed; inset:0; z-index:70; display:none;
  background-color:var(--oats,#FEF6E4);
  /* the desk: the reader's own faint rule + riso tooth, multiplied. This is print texture,
     not a blur effect — the ban is on glow/haze as a design device, and grain is the whole
     aesthetic's substrate. */
  background-image:
    repeating-linear-gradient(to bottom,rgba(65,77,87,.055) 0 1px,transparent 1px 23px),
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='220' height='220'%3E%3Cfilter id='g'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23g)' opacity='0.4'/%3E%3C/svg%3E");
  background-size:auto,220px 220px;
  background-blend-mode:normal,multiply;
  color:#414D57;
  --sv-sh:6px 7px 0 0 #414D57;
  --sv-sh-sm:3px 4px 0 0 #414D57;
  --sv-ease:cubic-bezier(.22,.86,.28,1);
  --sv-dur:430ms;
}
#shelfview.on{display:block;}

/* THE STAGE. In portrait the whole shelf is laid out landscape and turned 90°, so turning the
   phone is the only way to read it. No gyroscope, no permission prompt, no interstitial to
   dismiss — the instruction IS the layout. The reading layer below inverts this exactly. */
#sv-stage{position:absolute; left:0; top:0; width:100%; height:100%; transform-origin:0 0;}
@media (orientation:portrait){
  #sv-stage{width:100dvh; height:100vw; transform:rotate(90deg) translateY(-100%);}
}

/* ── the rack ───────────────────────────────────────────────────────────────────────────── */
#sv-scroll{
  position:absolute; inset:0; overflow-x:auto; overflow-y:hidden;
  -webkit-overflow-scrolling:touch; overscroll-behavior-x:contain; scrollbar-width:none;
  touch-action:pan-x;
}
/* THE TAKEOVER, applied by shelf.js once dragInit() has actually attached. Unconditionally, this
   would be a rack that cannot be scrolled by any means at all if the script ever threw — worse than
   the bug. Gated on the class, the CSS-only fallback stays the plain sideways scroller above. */
#shelfview.sv-drag #sv-scroll{touch-action:none;}
#sv-scroll::-webkit-scrollbar{display:none;}
#sv-track{position:relative; height:100%; padding-left:max(20px,env(safe-area-inset-left));}

/* the shelf plank — torn top edge, hard ink body */
#sv-ledge{position:absolute; left:0; right:0; height:15px; background:#414D57; pointer-events:none; z-index:30;}
#sv-ledge::before{content:""; position:absolute; inset:-2px -1px 0 -1px; background:#414D57; filter:url(#roughsm);}
#sv-ledge::after{content:""; position:absolute; left:0; right:0; top:0; height:2px; background:#FEF6E4; opacity:.5;}

.sv-item{position:absolute; top:0; height:100%; transform:translate3d(0,0,0);}
.sv-item.sv-moving{transition:transform var(--sv-dur) var(--sv-ease);}

/* THE FOLD — a zine standing edge-on. On a saddle-stitched zine the cover ink wraps the crease,
   so what you see IS the left edge of the front cover. Its WIDTH reports how long the zine is. */
/* The fold is now a BLOCK, not a rectangle: a cover face of constant width plus a fore-edge slab
   whose thickness is the zine's page count. Flat rectangles read as posters pinned to a wall; a
   visible edge is what makes them read as paper standing on a shelf. Cartoon-isometric rather
   than true perspective — every block is skewed by the same angle, so there is no vanishing point
   to get wrong (perspective-origin resolves against the scroll track, which is thousands of px
   wide, so real perspective put the vanishing point off-screen and every block rotated
   identically anyway). Same-angle skew is also simply more the house's cartoonish register. */
/* HAND-DRAWN EDGES. #roughsm is feTurbulence + feDisplacementMap, already in reader.html and
   already used on .tool::before and the plank. DESIGN.md's rule is that the filter rides the PLATE
   and never the content, so it goes on a ::before sitting a hair proud of the fold: the ink outline
   it draws is what wobbles, while the cover art, the type and the hit area stay perfectly sharp and
   perfectly rectangular.
   PERFORMANCE. One filter per visible book is real cost, and the rack scrolls horizontally. It is
   paint-only (no layout, no compositing change) and the plate is static, so it does not re-filter
   while the rack moves — but a turbulence per element is still the most expensive thing here, so it
   is switched off outright for reduced-motion and could be dropped to the resting rack if a real
   device disagrees. */
.sv-fold::before{
  content:""; position:absolute; inset:-4px; z-index:0; pointer-events:none;
  background:var(--sv-edge,#414D57); filter:url(#roughbk);
}
/* THE PLATE IS THE OUTLINE, so nothing may draw a straight one over it. .sv-slab and .sv-face each
   carried their own `3px solid` border, which sat exactly on top of the wobbling plate and buried
   it — a straight line drawn over a drawn one wins. Their borders are gone; the 4px of filtered
   plate showing past their edges IS the book's outline, and it takes the book's own colour. */
.sv-fold{
  position:absolute; left:0; top:var(--sv-top); bottom:var(--sv-bot);
  width:108px; cursor:pointer; transform-origin:50% 100%;
  padding:0; text-align:left; font:inherit; color:inherit;
  background:transparent; border:0;
}
/* THE COVER SLIVER — the little slice of cover turned toward you past the fold. It is a face
   rotated AWAY from the viewer, so it has to sit in shadow: a stripe rendered in the same plane
   and the same light as the spine beside it reads as a border, which is precisely what the
   cover-first version looked like. The shading is the whole depth cue. */
/* THE COVER — now the whole front of the zine, at its true width, lying across the zine before it.
   It is no longer a sliver turned away from the viewer, so the heavy shadow wash that used to be
   its depth cue is gone: a cover facing you should be lit. The overlap itself does the depth work,
   with one hard ink edge where each cover crosses the one behind. */
/* NO SHADING ANYWHERE ON THE RACK. Depth used to come from a tonal wash down the left of each
   cover — the one soft thing on an otherwise flat surface, and against DESIGN.md's whole register.
   The overlap is now read from the INK OUTLINE alone: a 3px black edge where each cover crosses the
   one behind is how a drawing says "in front", and it is the cartoon-skeuomorphic language the rest
   of the product speaks. Thicker than the 2.5px used on flat cards, because these edges are read at
   a glance across a whole rack rather than one card at a time. */
.sv-face{
  position:absolute; left:0; top:0; bottom:0; width:17px; overflow:hidden;
  background:#FEF6E4; border:0; z-index:2;
}
.sv-face .sv-cov{width:100%; max-width:none;}
/* THE SPINE. On the LEFT, because that is where the fold is on a saddle-stitched zine — the right
   edge is the fore-edge and carries no print. Its thickness is the page count, and it is the one
   surface in this view that can hold vertical type honestly: a dedicated printed panel, not
   rotated text laid over somebody's photograph. */
.sv-slab{
  position:absolute; top:0; bottom:0; left:0; z-index:2; overflow:hidden;
  background:#414D57; /* replaced inline by the zine's brand colour (brandFor + legibleGround) */
  border:0;
  display:flex; align-items:center; justify-content:center;
  /* room at the head for the pinned issue number */
  box-sizing:border-box; padding:22px 0 6px;
  /* the lit face: a hard ink offset so the whole block stands ON the plank */
  box-shadow:var(--sv-sh-sm);
  /* No skew. A 3° skew on a 265px panel reads as misalignment, not as depth — the eye needs a
     closing top face to interpret it as a box, and at this thickness that face is one pixel
     tall. A hard crease between spine and cover does the separating job honestly instead. */
}
/* the crease where the fold meets the cover — the one hard line that says "these are two faces
   of the same object" rather than "these are two rectangles side by side" */
/* THE BINDING'S EDGE IS DRAWN TOO. This crease is the fold where the cover wraps the block, so a
   ruler-straight line is the one thing it should not be. Same filter as the outline, same hand. */
.sv-slab::before{
  content:""; position:absolute; right:-1px; top:-2px; bottom:-2px; width:4px;
  background:var(--sv-edge,#414D57); filter:url(#roughbk);
}
/* THE PAGE-EDGE STRIPE IS GONE. It was a repeating cream gradient down the binding — a highlight,
   which is exactly the tonal trick this rack is supposed to be free of. Flat colour and a drawn
   edge carry it now. */
/* the printed title. Ink colour varies per zine — a real shelf is visually rich precisely because
   every publisher sets its spine differently. Only dark inks: Lazuli and Colorado both fail AA at
   this size on cream, so the variation runs Sambas / deepened Grass / the sea-chart sepia. */
/* WRAPS, over as many vertical lines as the band's thickness allows. A single nowrap line capped
   the label at whatever the fold's HEIGHT could carry (~31 characters), which spent itself on the
   club name and four words. Letting it wrap turns the band's width into extra lines — a thin zine
   gets two, a fat one three — so the challenge actually gets read. The block axis in vertical-rl
   is horizontal, so max-width (not max-height) is what bounds the line count. */
/* The CHALLENGE, in Pitch — the display face. It is a heading you read as a sentence, which
   DESIGN.md's component table names as precisely what the Dymo / Pitch Sans caps voice is not for.
   Sentence case for the same reason: a whole prompt shouted in caps is a wall, not a title. */
/* BITTER, not the display italic. Pitch (Courier Prime before it) carries the brand's voice, but
   it is a MONOSPACE italic — every letter boxed to the same width and slanted — and set vertically
   at 13px along a spine that is the least legible thing in the type stack. Bitter is DESIGN.md's
   body face: a slab serif drawn for small sizes, proportional, with real weight on the stems. It is
   still a Drex font; it is simply the one of the four that is built to be read. The challenge is a
   sentence, and a sentence wants the reading face. */
/* ONE FAMILY DOWN THE SPINE. Two faces jammed together with a middot — mono caps then a slab serif
   — is a lot of event for a 33px band, and it made the two halves fight instead of rank. Bitter
   throughout; WEIGHT carries the hierarchy, which is what weight is for. */
.sv-slab b{
  writing-mode:vertical-rl; text-orientation:mixed; transform:rotate(180deg);
  font-family:var(--body,"Bitter",Georgia,serif); font-style:normal;
  font-size:13px; line-height:1.3; color:var(--sv-ink,#414D57);
  overflow:hidden; max-height:100%; max-width:100%;
  padding:0 1px; text-align:left; overflow-wrap:break-word; white-space:pre-wrap;
}
.sv-slab b em{font-style:normal; font-weight:700;}                 /* the name */
.sv-slab b .sv-ch{font-weight:400; opacity:1;}                     /* what it was about */

/* THREE HOUSES. A rack is set by as many hands as it has publishers, so the variation is
   typographic and deterministic per zine — never a change of colour, size or contrast, only of
   how the name is set. */
.sv-fold.sv-h1 .sv-slab b em{text-transform:uppercase; letter-spacing:.07em; font-size:12px;}
.sv-fold.sv-h2 .sv-slab b em{letter-spacing:.005em;}
.sv-fold.sv-h3 .sv-slab b{font-family:var(--ui,"Pitch Sans","DM Mono",monospace); font-size:12px;}
.sv-fold.sv-h3 .sv-slab b em{font-weight:500; text-transform:uppercase; letter-spacing:.06em;}
/* The CLUB, in the label-maker voice — it is a structural label, which is what that voice IS for.
   INLINE inside the challenge, not positioned against it: percentage padding resolves against the
   containing block's WIDTH, so reserving "44%" of a 30-54px band bought ~20px where ~110px was
   needed and the two labels printed on top of each other. One flow, two voices, club first —
   masthead then title down the spine, the way print does it. */
.sv-slab b em{
  font-family:var(--ui,"Pitch Sans","DM Mono",monospace); font-weight:500; font-style:normal; font-size:12px;
  letter-spacing:.06em; text-transform:uppercase;
  /* NO alpha. Fading the club name to .72 took it to 2.21:1 on a mid-tone cover — undoing the very
     thing legibleGround() is computing. Hierarchy comes from the face and the case: Pitch Sans caps
     against the display italic already reads as label-then-title. */
}


/* THE PASTED CREAM LABEL IS GONE. It was variation by BACKGROUND — a white plate on one book in
   three — which on a rack of flat colour reads as a hole rather than as a different press. The
   variation lives in the TYPE now (.sv-h1/.sv-h2/.sv-h3 below), which is where a publisher's
   difference actually shows. */
/* The issue number, pinned at the head of the band and OUT of the wrapping flow, so no amount of
   challenge text can push it off. Set horizontally: a two-or-three character number reads fine
   across a 30-54px band, and rotating it would make the one precise token the hardest to scan. */
.sv-slab i{
  position:absolute; left:0; right:0; top:4px; z-index:1;
  writing-mode:horizontal-tb; text-orientation:mixed; transform:none;
  font-family:var(--ui,"Pitch Sans","DM Mono",monospace); font-weight:500; font-size:12px; font-style:normal;
  line-height:1; text-align:center; color:var(--sv-ink,#414D57);
}
#shelfview.sv-narrow .sv-slab b{display:none;}
.sv-item.sv-moving .sv-fold{transition:width var(--sv-dur) var(--sv-ease), opacity .16s linear;}
/* Resting tilt is systematic, not random — a rack that reshuffles on every render stops reading
   as objects that were put there and starts reading as a screensaver. The LEAN on top of it is
   live and comes from scroll velocity, so the rack tips against a fling and settles back. */
.sv-fold{
  transform:rotate(calc(var(--sv-tilt,0deg) + var(--sv-lean,0deg)));
  transition:transform .34s cubic-bezier(.22,.86,.28,1);
}
.sv-item:nth-child(odd)  .sv-fold{--sv-tilt:.7deg;}
.sv-item:nth-child(4n+2) .sv-fold{--sv-tilt:-1.1deg;}
.sv-item.sv-moving .sv-fold{
  transition:width var(--sv-dur) var(--sv-ease), opacity .16s linear,
             transform .34s cubic-bezier(.22,.86,.28,1);
}
@media (prefers-reduced-motion:reduce){ .sv-fold{transform:rotate(var(--sv-tilt,0deg));} }
.sv-fold:focus-visible{outline:3px solid #2496C9; outline-offset:-3px;}

/* ANTICIPATION. You do not make a zine open by pointing at it — you pull it half out of the rack
   first, and it comes up and toward you. Without that beat the fold just inflates, which is the
   difference between an object and a div. 130ms is enough to register and short enough that it
   reads as part of the tap rather than as lag. */
.sv-fold.sv-pull{
  transform:translateY(-9px) rotate(calc(var(--sv-tilt,0deg) - 1.6deg)) scale(1.015);
  box-shadow:7px 9px 0 0 #414D57;
  transition:transform .13s cubic-bezier(.34,1.3,.5,1), box-shadow .13s ease-out;
  z-index:20;
}
/* SECONDARY MOTION. The neighbours do not slide rigidly aside; the gap opens and they tip into
   it, then right themselves. Applied only to the few nearest the opening — past that it is noise
   nobody sees. */
.sv-item.sv-tip .sv-fold{--sv-tilt:-3.2deg;}
@media (prefers-reduced-motion:reduce){
  .sv-fold.sv-pull{transform:rotate(var(--sv-tilt,0deg));}
  .sv-item.sv-tip .sv-fold{--sv-tilt:0deg;}
}

/* the cover, positioned so the fold shows its LEFT edge */
/* At this density the fold is wide enough to show a real crop of the cover rather than a sliver
   of one, so the art fills it and does the identifying work it was made for. */
.sv-cov{position:absolute; inset:0; width:100%; height:100%; object-fit:cover; object-position:center top;
  display:block; background:#FEF6E4;}
/* the fore-edge: stacked paper edges of a folded pamphlet, at the open side */
.sv-fore{
  position:absolute; right:0; top:0; bottom:0; width:6px; z-index:3; background:#FEF6E4;
  background-image:repeating-linear-gradient(to right,rgba(65,77,87,.22) 0 1px,transparent 1px 3px);
  border-left:1px solid #414D57;
}
/* READ is a thumbed sheet, never a greyed-out one — torn/warp means read */
.sv-fold.sv-read .sv-cov{filter:url(#warp);}
.sv-fold.sv-read .sv-fore{background-color:#F2E9D4;}

/* A small CLUB credit at the cover's foot. The title itself is printed on the spine now, where
   a title belongs — six identical dark plates along the bottom of the rack formed a continuous
   band that competed with the plank right beneath it and read as UI bolted onto somebody else's
   artwork. Kept only when the club name is not just restating the title, which for club-named
   issues it usually is. */
.sv-plate{
  position:absolute; z-index:4; left:0; right:0; bottom:0;
  background:#414D57; color:#FEF6E4; padding:6px 8px; display:block; overflow:hidden;
  font-family:var(--ui,"Pitch Sans","DM Mono",monospace); font-weight:500; font-size:12px;
  letter-spacing:.05em; text-transform:uppercase; white-space:nowrap; text-overflow:ellipsis;
}

/* the HEAD — the closed top edge of a folded pamphlet. Sells "a stack of paper standing up"
   without a single polygon, and it is the tell that separates this from a row of posters. */
.sv-head{
  position:absolute; left:0; right:0; top:0; height:5px; z-index:3; background:#FEF6E4;
  background-image:repeating-linear-gradient(to bottom,rgba(65,77,87,.2) 0 1px,transparent 1px 2px);
  border-bottom:1px solid rgba(65,77,87,.55);
}

/* the live beacon. Happy = Club hour live AND someone present. Flat enamel coin, hard ink
   border, NO glow — the halo is banned and the pilot-lamp language is the house alternative. */
.sv-lamp{
  position:absolute; z-index:5; top:11px; left:50%; margin-left:-6px;
  width:12px; height:12px; border-radius:50%; background:#F1DD01; border:1.6px solid #414D57;
}
/* an open challenge is Colorado, and it is a different claim from "live" — a torn corner flag */
.sv-flag{
  position:absolute; z-index:5; top:9px; left:0; width:0; height:0;
  border-top:13px solid #F75458; border-right:13px solid transparent;
}

/* ── the opened spread ──────────────────────────────────────────────────────────────────── */
/* THE OPENED SPREAD IS DRAWN TOO. It kept a ruler-straight 2px border while every closed book on
   the rack around it had a hand-drawn edge — so opening one swapped craft mid-gesture, which is
   exactly the "all of a sudden it has clean lines" jump. Same plate, same filter, same colour as
   the book it came out of: the object you opened is the object you were looking at.
   The plate cannot live on .sv-spread itself, because that element is clip-path'd open from zero
   width and a filtered ::before would be clipped along with it — it goes on the ITEM, which is not
   clipped, and shows only while the spread is open. */
.sv-spread{
  position:absolute; left:0; top:var(--sv-top); bottom:var(--sv-bot); width:var(--sv-spread);
  clip-path:inset(0 100% 0 0); display:flex; opacity:0; pointer-events:none;
  background:#FEF6E4; border:0; box-shadow:var(--sv-sh);
}
/* Sized to the SPREAD, not with left/right insets. .sv-item is absolutely positioned with no width
   and only absolutely-positioned children, so its own box shrink-to-fits to ZERO — `right:-4px`
   against that produced an 8px sliver rather than an outline. */
.sv-item.sv-open::before{
  content:""; position:absolute; z-index:0; pointer-events:none;
  left:-4px; top:calc(var(--sv-top) - 4px); bottom:calc(var(--sv-bot) - 4px);
  width:calc(var(--sv-spread) + 8px);
  background:var(--sv-open-edge,#414D57); filter:url(#roughbk);
}
.sv-item.sv-moving .sv-spread{transition:clip-path var(--sv-dur) var(--sv-ease), opacity .16s linear;}
.sv-item.sv-open .sv-spread{opacity:1; pointer-events:auto;}
.sv-item.sv-open .sv-fold{opacity:0; pointer-events:none;}

/* left column: NOT the cover — the zine's own first screen is its cover, so repeating it here
   would repeat the thing the iframe is about to draw. This carries what the document cannot:
   the challenge that prompted it, and who is in the club RIGHT NOW. */
/* TWO EQUAL PAGES. The spread is a zine lying open: the left page carries who made it and what the
   challenge was, the right page is the cover. An open book does not have one page half the width of
   the other, and the 34/66 split made the meta read as a sidebar bolted to the artwork rather than
   as the facing page. */
.sv-side{
  flex:0 0 50%; min-width:0; max-width:none; padding:16px 15px; overflow-y:auto;
  border-right:2px solid #414D57; background:#FEF6E4; display:flex; flex-direction:column; gap:12px;
}
.sv-side .sv-eyebrow{
  font-family:var(--ui,"Pitch Sans","DM Mono",monospace); font-weight:500; font-size:12px; letter-spacing:.14em;
  text-transform:uppercase; opacity:.62; margin:0;
}
/* THE ONE ITALIC LEFT ON THE RACK. This is the zine's title on the spread that opens as its cover,
   and a cover title is on the short list the display italic exists for. The other thing that used
   to be set in it here — the shelf's corner label — is upright now. Pitch Semibold Italic is a real
   face, declared over 600-700 in src/app/fonts.css, so the 700 below maps onto it rather than
   slanting the upright Bold. */
.sv-side h3{
  font-family:var(--fd,"Pitch","Courier Prime",monospace); font-style:italic; font-weight:700;
  font-size:1.05rem; line-height:1.06; margin:0; text-wrap:balance;
}
/* The challenge, marker-swiped. A solid School Bus fill here blew the colour budget when the
   same treatment sat behind every heading in the app (DESIGN.md decision log, 2026-06-26): the
   fix there was a ~40% amber wash, which still reads as a highlighter without dominating. Note
   the plate is drawn by ::before with the rough filter, and the element itself must stay
   TRANSPARENT — an opaque background on the parent paints straight over its own z-index:-1
   pseudo, which is exactly how the first version rendered as plain text. */
.sv-side .sv-chal{
  font-family:var(--body,"Bitter",Georgia,serif); font-weight:300; font-size:.76rem; line-height:1.5;
  margin:0; padding:8px 9px; background:transparent; position:relative; z-index:0;
}
.sv-side .sv-chal::before{
  content:""; position:absolute; inset:-1px -3px; z-index:-1;
  background:rgba(236,175,33,.42); filter:url(#roughsm); transform:skewX(-1.5deg);
}
.sv-side .sv-meta{
  font-family:var(--ui,"Pitch Sans","DM Mono",monospace); font-weight:500; font-size:12px; letter-spacing:.06em;
  opacity:.72; margin:0; line-height:1.7;
}
.sv-side .sv-livechip{
  display:inline-flex; align-items:center; gap:6px; align-self:flex-start;
  background:#F1DD01; color:#4a3410; border:2px solid #414D57; padding:5px 9px;
  font-family:var(--ui,"Pitch Sans","DM Mono",monospace); font-weight:500; font-size:12px;
  letter-spacing:.08em; text-transform:uppercase; box-shadow:var(--sv-sh-sm);
}
.sv-side .sv-livechip b{width:8px; height:8px; border-radius:50%; background:#414D57;}

.sv-doc{flex:1 1 auto; position:relative; background:#FEF6E4; overflow:hidden;}
.sv-doc iframe{position:absolute; inset:0; width:100%; height:100%; border:0; background:#FEF6E4;}
/* the poster: the cover, shown while the spread is still opening. The live iframe is swapped in
   ON ARRIVAL — never animate a container that holds a live document, or the guest re-lays-out
   its 100vh bands on every frame of the transition. */
.sv-poster{position:absolute; inset:0; width:100%; height:100%; object-fit:cover; object-position:top center;
  transition:opacity .3s ease; z-index:2;}
.sv-doc.sv-lit .sv-poster{opacity:0; pointer-events:none;}

.sv-shut{
  position:absolute; z-index:6; top:8px; right:8px; width:34px; height:34px;
  background:#FEF6E4; color:#414D57; border:2px solid #414D57; cursor:pointer;
  display:none; align-items:center; justify-content:center; padding:0; box-shadow:var(--sv-sh-sm);
}
.sv-item.sv-open .sv-shut{display:flex;}
.sv-shut:active{transform:translate(3px,4px); box-shadow:none;}
.sv-shut:focus-visible{outline:3px solid #2496C9; outline-offset:2px;}

/* the end of the rack is an OBJECT, never an empty gap */
#sv-endcap{position:absolute; top:var(--sv-top); bottom:var(--sv-bot); width:250px; display:flex;
  align-items:flex-end; padding:0 0 26px 24px;}
/* "that's the whole shelf — for now" is an aside written at the end of the row, not a label on
   anything — the one place on the rack where a marker in a margin is the honest instrument. So it
   stays Patrick Hand, which is what --font-scrawl carries; the caption hand (Birdie) belongs on the
   plates and eyebrows. The token was --fh, which no document that loads this file has ever defined
   — the literal fallback was doing all the work. --font-scrawl is the name the app uses for it. */
#sv-endcap span{
  font-family:var(--font-scrawl,"Patrick Hand","Comic Sans MS",cursive); font-size:1.05rem; color:#414D57; opacity:.72;
  transform:rotate(-2deg); display:block; max-width:190px; line-height:1.3;
}

/* THE 12px LABELS, HELD AT THE SIZE THEY WERE DRAWN AT.
   Every rule above that reads `var(--ui, …)` sets 12px, and Pitch Sans's cap-height is 0.570em
   against DM Mono's 0.700 — 18.6% shorter. At 12px that is 8.4px of painted capital dropping to
   6.8px, on the smallest type on the rack, several rules of which are uppercase with tracking.
   `cap-height 0.70` restates DM Mono's ratio, so they paint at the size these rules were tuned
   against; it is a no-op on DM Mono itself (whose cap-height IS 0.70), so it changes nothing until
   the real face loads, and browsers without the two-value syntax ignore it.

   THE TWO SPINE RULES ARE DELIBERATELY NOT IN THIS LIST. `.sv-slab b em` and `.sv-fold.sv-h3
   .sv-slab b` are set vertically inside a band whose capacity public/reader/shelf.js MEASURES
   (spineCapacity, CH_W = 6.9) before deciding how much of the challenge to hand over. Restoring
   cap-height scales the advance with it (~+23%), which would put the spine well over a budget
   computed in JS — and vertical writing-mode has no text-overflow:ellipsis to catch the overrun, it
   just clips mid-word. The spine keeps Pitch Sans at its natural size. */
.sv-slab i, .sv-plate, .sv-side .sv-eyebrow, .sv-side .sv-meta, .sv-side .sv-livechip,
#sv-label i, #sv-exit, #sv-turn, .sv-open-cta{
  font-size-adjust:cap-height 0.70;
}

/* ── chrome ─────────────────────────────────────────────────────────────────────────────── */
#sv-label{position:absolute; z-index:40; left:max(20px,env(safe-area-inset-left)); top:max(13px,env(safe-area-inset-top)); transform:rotate(-1.4deg);}
/* UPRIGHT, not italic. Pitch Bold upright is the HEADING face (brand PDF p36 "Hierarchy"); the
   Semibold Italic is the DISPLAY face (p33) and is reserved for entrances — the front door, a share
   card, a zine's own cover title. "Fresh" / "Club hour" in the corner of the shelf is a sign saying
   which rack you are standing at, which is a heading doing its job, not an entrance. */
#sv-label b{display:block; font-family:var(--fd,"Pitch","Courier Prime",monospace); font-weight:700; font-size:1.12rem; line-height:1;}
#sv-label i{display:block; font-style:normal; font-family:var(--ui,"Pitch Sans","DM Mono",monospace); font-weight:500; font-size:12px; letter-spacing:.13em; text-transform:uppercase; opacity:.6; margin-top:4px;}
#sv-exit{
  position:absolute; z-index:41; right:max(16px,env(safe-area-inset-right)); top:max(13px,env(safe-area-inset-top));
  display:flex; align-items:center; justify-content:center; gap:7px;
  background:#FEF6E4; color:#414D57; border:2px solid #414D57;
  /* 44px minimum touch target. It was 92x30 — wide enough to read, too short to hit reliably
     with a thumb, and this is the ONLY way back out of the shelf. */
  min-height:44px; padding:7px 13px; cursor:pointer; box-shadow:var(--sv-sh-sm);
  font-family:var(--ui,"Pitch Sans","DM Mono",monospace); font-weight:500; font-size:12px; letter-spacing:.11em; text-transform:uppercase;
}
#sv-exit:active{transform:translate(3px,4px); box-shadow:none;}
#sv-exit:focus-visible{outline:3px solid #2496C9; outline-offset:2px;}

/* ── the turn: browse sideways, read upright ────────────────────────────────────────────── */
/* inset:0 rather than left/top:50% + translate + 100vw/100dvh. The resting box is then EXACTLY the
   viewport by construction, with no viewport-unit arithmetic to be a pixel out and nothing for a
   half-finished transform to leave behind — which is what "sometimes it doesn't tween to full
   height, there's a little margin at the bottom" was. The tween is now purely a scale about the
   centre, so however it is interrupted it still RESTS full-bleed. */
#sv-reader{
  position:fixed; z-index:80; inset:0;
  background:#FEF6E4; border:2px solid #414D57; box-shadow:var(--sv-sh);
  /* Starts small and slightly cocked, lands square and full. The rotation is what makes it read as
     a thing being PICKED UP off the rack rather than a panel being resized. */
  transform:rotate(-2.2deg) scale(.82); transform-origin:50% 50%;
  opacity:0; display:none; overflow:hidden;
  transition:opacity .26s linear, transform .56s var(--sv-ease);
}
#sv-reader.sv-live{display:block;}
#sv-reader.sv-on{opacity:1;}
/* WHEN THE PHONE ITSELF TURNS, DO NOT ANIMATE.
   The reading layer's rotation is defined by an orientation media query, so physically turning
   the device swaps which rule applies — and with a 560ms transition on transform, our rotation
   then animates AGAINST the OS's own rotation animation. The result is the page visibly skewing
   through ~30° of diagonal while the system is also rotating it: two animations, one object,
   fighting. The turn is ours to animate; the device turning is the OS's. */
#shelfview.sv-orienting #sv-reader,
#shelfview.sv-orienting #sv-stage{transition:none !important;}
/* THE READING LAYER NO LONGER ROTATES, in either orientation.
   It used to turn 90° in landscape on the theory "browse sideways, read upright" — but the shelf
   is a landscape view, so in practice the zine you opened came up with its text running down the
   side of the screen and a "turn it back up" button to undo it. Meanwhile the book reader at "/"
   never rotates anything: in landscape it reads as a wide short column, via refW()'s 270-560 clamp
   and nothing else.
   "/" is the source of truth for how a zine reads, so this matches it. It also retires the whole
   .sv-orienting hack, which existed only because this rotation animated against the device's own. */
#sv-reader.sv-on{transform:none;}
#sv-reader iframe{position:absolute; inset:0; width:100%; height:100%; border:0; background:#FEF6E4;}
#shelfview.sv-reading #sv-stage{opacity:.16; transform:scale(.97); pointer-events:none;}
#sv-stage{transition:opacity .32s linear, transform .5s var(--sv-ease);}

/* "turn it back up" is RETIRED. It only ever existed to explain the 90° rotation this layer used
   to apply, and with the rotation gone it was a coach mark for a thing that no longer happens —
   the worst kind, because it tells you to fix something that is not broken. */
#sv-turnback{display:none !important;}
/* LEFT, not right: the bookmark hangs off the right edge of the reading column on every surface,
   and a 36px close button parked on top of it made the ribbon both unreadable and ungrabbable at
   the very position a hand reaches for it first. Exit left, place-marker right. */
#sv-readshut{
  position:absolute; z-index:6; top:11px; left:11px; width:44px; height:44px;
  /* Colorado. The exit is the one destructive-ish control on a full-bleed reading surface — it
     throws away where you were — and it has to be findable over artwork of any colour, which cream
     on cream is not. Oats ink on Colorado clears AA at this size. */
  background:#F75458; color:#FEF6E4; border:2px solid #414D57; cursor:pointer;
  display:flex; align-items:center; justify-content:center; padding:0; box-shadow:var(--sv-sh-sm);
}
#sv-readshut:active{transform:translate(3px,4px); box-shadow:none;}

/* the portrait nudge, upright OUTSIDE the rotated stage so it reads correctly */
/* "TURN YOUR PHONE" — a one-shot, dead centre, that turns and goes.
   It used to sit permanently at bottom-centre, which is exactly where #clubBtn.circ-hud lives
   (left:50%, bottom:16px) — so the orb sat on top of it and the instruction was unreadable.
   Centre is where an instruction belongs anyway, and this one should not be furniture: it asks you
   to do something, and once you have done it there is nothing left for it to say. So it fades in
   upright, holds long enough to read, TURNS 90deg — the same direction and angle as the rack, so
   the gesture is demonstrated rather than described — and leaves. */
#sv-turn{
  position:absolute; z-index:95; left:50%; top:50%;
  transform:translate(-50%,-50%); display:none; align-items:center; gap:8px;
  background:#414D57; color:#FEF6E4; padding:10px 16px; box-shadow:var(--sv-sh-sm);
  font-family:var(--ui,"Pitch Sans","DM Mono",monospace); font-weight:500; font-size:12px;
  letter-spacing:.13em; text-transform:uppercase; white-space:nowrap; pointer-events:none;
}
@media (orientation:portrait){
  #shelfview.on #sv-turn{display:flex; animation:svturn 3.6s var(--sv-ease) forwards;}
}
@keyframes svturn{
  0%   {opacity:0; transform:translate(-50%,-50%) rotate(0deg)  scale(.92);}
  10%  {opacity:1; transform:translate(-50%,-50%) rotate(0deg)  scale(1);}
  42%  {opacity:1; transform:translate(-50%,-50%) rotate(0deg)  scale(1);}
  70%  {opacity:1; transform:translate(-50%,-50%) rotate(90deg) scale(1);}
  86%  {opacity:1; transform:translate(-50%,-50%) rotate(90deg) scale(1);}
  100% {opacity:0; transform:translate(-50%,-50%) rotate(90deg) scale(.96);}
}
#shelfview.sv-reading #sv-turn{display:none !important;}

@media (prefers-reduced-motion:reduce){
  /* The roughen filter is the most expensive paint on the rack — one feTurbulence per visible book.
     It is decoration, so reduced-motion (which correlates with "this device is struggling" far more
     often than the name suggests) gets the straight ink edge instead. Measured at 59.2fps with 40
     books filtered on desktop, so this is insurance rather than a known failure. */
  .sv-fold::before{filter:none;}
  /* A 90deg spin is a vestibular event. The instruction still needs to be READ, so it fades in and
     out in place — the turn is demonstrated by the rack itself, which is already sideways. */
  #shelfview.on #sv-turn{animation:svturnstill 3.6s linear forwards;}
  @keyframes svturnstill{
    0%{opacity:0;} 8%{opacity:1;} 80%{opacity:1;} 100%{opacity:0;}
  }
  .sv-item.sv-moving, .sv-item.sv-moving .sv-fold, .sv-item.sv-moving .sv-spread{transition-duration:1ms !important;}
  /* a 90° spin is a vestibular event — reduced motion gets a cut, not a turn */
  #sv-reader{transition:opacity .12s linear !important;}
  #sv-stage{transition:opacity .12s linear !important;}
}

/* ── "Read this →" — the invitation, printed on the cover ─────────────────────────────────────
   The spread shows a cover, and a cover does not look tappable. On a phone there is no hover to
   discover it with, and a one-time coach mark is worse than useless here: it teaches a gesture,
   then withdraws the only evidence the gesture exists. So this is permanent furniture, in the
   house's rubber-stamp language — ink border, flat offset, a small tilt, press-in on :active. */
.sv-open-cta{
  position:absolute; left:50%; bottom:14px; z-index:4;
  transform:translateX(-50%) rotate(-1.4deg);
  display:flex; align-items:center; gap:7px;
  padding:9px 14px; pointer-events:none;          /* the whole cover is the target, not just this */
  background:#FEF6E4; color:#414D57;
  border:2.5px solid #414D57; border-radius:6px; box-shadow:3px 4px 0 0 #414D57;
  font-family:var(--ui,"Pitch Sans","DM Mono",monospace); font-weight:500; font-size:12px;
  letter-spacing:.1em; text-transform:uppercase; white-space:nowrap;
  transition:transform .14s ease, box-shadow .14s ease;
}
.sv-open-cta b{font-weight:500; font-size:13px;}
.sv-doc:hover .sv-open-cta{transform:translateX(-50%) rotate(0deg);}
.sv-doc:active .sv-open-cta{transform:translateX(-50%) rotate(-1.4deg) translate(3px,4px); box-shadow:0 0 0 0 #414D57;}
.sv-doc:focus-visible{outline:3px solid #2496C9; outline-offset:-3px;}
/* Once a document is actually mounted the invitation has been accepted — it must not sit on top of
   the zine it opened. */
.sv-doc.sv-lit .sv-open-cta{opacity:0;}
