/* sketchbook.css — the book: paper, printed photos, and the turning leaf.
 *
 * The curl is a port of the CSS page-turn from the ThreeUI sketchbook landing
 * page (meng-to-sketchbook.html, sha256 e0330548b1ac905c…5b6dd1), lines
 * 173-240 of that file. Its geometry is kept exactly — 18 nested strips, each
 * rotated a share of the curl's arc, each carrying a front and a back face —
 * because the numbers in it are tuned and rederiving them by eye is a week
 * nobody has.
 *
 * TWO THINGS ARE DELIBERATELY NOT THE REFERENCE, and both follow from the same
 * fact: its pages are single full-bleed plate IMAGES, and ours are real content.
 *
 *   1. Its `.face` paints the plate with `background-image` plus a
 *      `background-position-x` per strip. A page here is a DOM subtree, so the
 *      face is an `overflow: hidden` window over a full-width copy of the page,
 *      shifted by that same offset. Everything else about the face — the
 *      backface culling, the `right: -1.1px` seam bleed, the shade and gleam
 *      overlays — is the reference's.
 *
 *   2. Its shading is inset by `--pg: 21.8%`, because its plate images are
 *      photographs of a book with a margin of desk around the paper. Our page
 *      IS the paper, edge to edge, so `--pg` is 0 here. Leave the `var(--pg, …)`
 *      fallbacks in place: they are load-bearing in the reference and cost
 *      nothing here.
 *
 * The generic reference names (.curl, .strip, .face, .sh, .gl) are prefixed
 * `sb-` — four stylesheets share this page and `.face` is not a name anyone can
 * claim safely.
 */

/* ---------- the layer ----------
 *
 * [hidden] IS OVERRIDDEN BY `display: flex` AND HAS TO BE SAID AGAIN. The
 * attribute's whole effect is a UA rule of `display: none`, which any author
 * `display` beats on specificity. index.html ships this element hidden and the
 * task that reveals it toggles that attribute, so without the second rule the
 * book would be on screen over the desk from the first paint, with the
 * attribute still sitting there saying otherwise. Checked by removing this rule
 * and reloading: the book covers the desk.
 */
.sketchbook-layer {
  position: fixed;
  inset: 0;
  /* Above the focused photo's brass tag (7000) and below the trunk form's
     backdrop (7500), the studio (7800-7950) and the lock screen (10000) — so a
     locked page stays locked. The full ladder is documented in trunks.css. */
  z-index: 7200;
  display: flex;
  align-items: center;
  justify-content: center;
  /* The book's own `ground` from SketchbookModel.buildBooks, written by
     Sketchbook.open and taken off again by close(). THE FALLBACK IS THE
     SHELF'S OWN CASE, not the pale parchment it used to be: the shelf paints a
     case that fills this layer, so all this colour ever does now is sit
     underneath it, and a light band showing past a dark case at some size
     nobody tested is worse than a colour nobody sees. It is not a second copy
     of that colour either — `--sb-case` is declared once, in the shelf block
     below, on this same element. */
  background: var(--sb-ground, var(--sb-case));
  transition: background-color .45s ease;
  overflow: auto;
}
.sketchbook-layer[hidden] { display: none; }

.sketchbook-layer {
  /* ---- THE ONE DIAL: how old the paper is ----------------------------------
   *
   * 0 is a fresh sheet, 1 is as aged as this stylesheet goes, and everything
   * between is a straight blend of the two — the tone, the mottle, the folds,
   * the fibre and the grubby edges all move together, because `--sb-veil`
   * below is a wash of the FRESH colour laid over the finished aged sheet at
   * `1 - --sb-age`. That is a linear interpolation, so this really is one
   * number and not six that have to be re-tuned in step.
   *
   * Change this line and nothing else to take it up or down. Shipped at .62:
   * aged at a glance, still obviously paper.
   *
   * IT IS ON THE LAYER, NOT ON `.sb-page`, so a page copy inside a turning
   * leaf inherits the same value as the page it was cloned from — a leaf that
   * aged at a different rate mid-turn would be very hard to miss. */
  --sb-age: .62;

  /* The two ends of that blend. `--paper` is the sheet at --sb-age: 1 and is
   * deliberately deeper than the tone you see on screen: the veil lifts it
   * back toward `--sb-paper-fresh`, and at .62 the two land around #ddc9a4,
   * which is the coffee the owner asked for. Picked against a real drawing on
   * a real spread, not from a swatch. */
  --paper: #dcc79b;
  --sb-paper-fresh: #f2ebdd;
  --sb-veil: rgba(242, 235, 221, calc(1 - var(--sb-age)));

  /* The coffee every ageing layer is made of, as bare channels so the layers
   * can each pick their own alpha out of `--sb-age`. A single hue throughout:
   * the owner asked for the whole sheet to carry the tone, not for blots. */
  --sb-stain: 92, 62, 26;
  /* The light side of a fold — the ridge that catches the light. Warm white,
   * because the ridge is still paper. */
  --sb-ridge: 255, 250, 236;

  --sb-ink: #2b2721;
  --sb-ink-soft: rgba(43, 39, 33, .58);
  --sb-ink-faint: rgba(43, 39, 33, .36);

  /* ---- the two drawn surfaces, as inline SVG -------------------------------
   *
   * NOTHING IS FETCHED. Both are `data:` URIs built out of feTurbulence, which
   * is the only way to get noise with no file: the site has no CDN, has never
   * had one, and whether a bitmap goes into the repo is the owner's decision.
   * Only the `#` of the filter reference is percent-encoded (%23) — it is the
   * one character a `url()` genuinely may not carry raw.
   *
   * `color-interpolation-filters='sRGB'` is on both and IT IS THE MEAN THAT
   * DEPENDS ON IT, not the contrast. Rendered both ways into a canvas and
   * measured: the fibre tile comes out mean grey 125.8, sd 12.5 with it, and
   * mean 186.2, sd 8.3 without. soft-light treats 127.5 as the neutral that
   * changes nothing, so the sRGB version leaves the paper's tone alone while
   * the linearRGB default would lighten every page. Less contrasty, not more —
   * which is the opposite of what this comment said before someone measured.
   *
   * FIBRE — the tooth, tiled at 200px. `stitchTiles='stitch'` makes the
   * turbulence periodic over exactly the filter region so the tile joins
   * itself. IT IS INSURANCE HERE RATHER THAN A RESCUE, and the measurement
   * says so: drawing the tile twice side by side, the mean step across the join
   * is 10.9 levels with stitching and 13.1 without, against 15.3 for an
   * ordinary neighbouring-pixel step INSIDE a tile. At baseFrequency .8 the
   * noise is fine enough that neither version shows a line. Keep it anyway:
   * drop baseFrequency and the unstitched join is what starts to show first.
   *
   * feFuncA pins alpha to 1. Without it the tile measures mean alpha 127 rather
   * than 255 — half the layer's effect thrown away, and the half that survives
   * modulated by a second noise field independent of the grey one. The greys
   * are squeezed into .22–.77 around a neutral .5, which is what soft-light
   * needs to leave the mean tone alone.
   *
   * MOTTLE — the uneven ageing, ONE copy stretched over the whole spread and
   * never repeated, so there is no tile and therefore no tile seam at the
   * gutter. Its 400x142 is the spread's own 2000x710, so it stretches without
   * distorting. The feColorMatrix throws away the turbulence's colour and keeps
   * one channel as ALPHA over a fixed coffee — so the sheet is one stain
   * varying in strength, not blotches of different colours. */
  --sb-fibre: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='200' height='200'><filter id='f' x='0' y='0' width='100%25' height='100%25' color-interpolation-filters='sRGB'><feTurbulence type='fractalNoise' baseFrequency='.8' numOctaves='3' stitchTiles='stitch'/><feColorMatrix type='saturate' values='0'/><feComponentTransfer><feFuncR type='linear' slope='.55' intercept='.22'/><feFuncG type='linear' slope='.55' intercept='.22'/><feFuncB type='linear' slope='.55' intercept='.22'/><feFuncA type='linear' slope='0' intercept='1'/></feComponentTransfer></filter><rect width='200' height='200' filter='url(%23f)'/></svg>");
  --sb-mottle: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='400' height='142'><filter id='m' x='0' y='0' width='100%25' height='100%25' color-interpolation-filters='sRGB'><feTurbulence type='fractalNoise' baseFrequency='.018' numOctaves='3' seed='11'/><feColorMatrix type='matrix' values='0 0 0 0 .361 0 0 0 0 .243 0 0 0 0 .102 .34 0 0 0 -.085'/></filter><rect width='400' height='142' filter='url(%23m)'/></svg>");
}

/* ---------- the shelf ----------
 *
 * WHAT THE VIEW OPENS ON, and the seam a later plan replaces with a shelf drawn
 * in three dimensions. Everything here is under `.sb-shelf`, and the only thing
 * in sketchbook.js that builds any of it is renderShelf — so that plan swaps one
 * function and this one block, and nothing else in either file moves.
 *
 * A COVER IS BOUND IN ITS TRUNK'S HIDE. `--sb-leather` and `--sb-leather-deep`
 * are the trunk's own --leather-1 and --leather-2 (SketchbookModel carries them
 * over from trunks.css), and the layer stack below is the one on `.trunk-lid`,
 * reduced to what survives at cover size: the key light from the upper left, the
 * three grain passes at coprime periods, and the edge vignette that stops it
 * reading as a flat card. It is deliberately the same recipe — a book made out
 * of a trunk that did not look like the same leather would be the whole idea
 * failing at the first glance.
 *
 * NO BLUR AND NO BLEND MODE ANYWHERE IN THIS BLOCK. The aged paper in the book
 * already costs a page turn seven frames over 16.7ms from its layers and its
 * `soft-light`; the shelf is a different screen and must not add to what a turn
 * has to live with. Everything here is a gradient, a border or a box-shadow —
 * static, painted once, and gone from the tree the moment a book is opened.
 */

/* THE CASE, AND IT FOLLOWS THE DESK THE USER CHOSE. The layer is opaque and
   covers the viewport, so neither background can show through and neither is
   "wrong" — but the corner controls sit on top of the shelf and they ARE
   themed, and each pill was drawn against its own theme's values. The wood
   theme's pill is dark brown with a cream glyph; the parchment theme's is
   cream with dark ink. Pinning the case to one colour would hand one of the
   two a pill it was never designed against. So the case takes the same walnut
   the wood desk is made of when the desk is wood, and a warmer, lighter hide of
   the same wood otherwise.
   The variables are on the LAYER rather than on `.sb-shelf` because the layer's
   own `background` falls back to `--sb-case` (see the note up there) — one
   definition, read from both places. */
.sketchbook-layer {
  --sb-case: #33241a;
  --sb-case-deep: #1b120b;
  --sb-board-lip: #a87f52;
  --sb-board-face: #6f4e30;
  --sb-board-foot: #33220f;
  --sb-case-ink: rgba(232, 214, 182, .72);
}
body[data-bg="wood"] .sketchbook-layer {
  /* #5e3924 and #4a2818 are the polished-wood desk's own two planks, taken from
     style.css so the cabinet and the desk it stands on are the same timber. */
  --sb-case: #2b1a10;
  --sb-case-deep: #150c06;
  --sb-board-lip: #8f6a41;
  --sb-board-face: #5e3924;
  --sb-board-foot: #2a1710;
  --sb-case-ink: rgba(244, 224, 192, .78);
}

/* `margin: auto` AND NOT THE LAYER'S `align-items: center`, which is what would
   otherwise centre this. Flex centring puts the overflow of a too-tall child
   above the scroll origin, so the first row of a shelf taller than the window
   cannot be scrolled to at all; an auto margin is defined to become 0 when the
   free space is negative, so the shelf simply starts at the top and scrolls.
   `min-height: 100%` is what makes the case fill the window when there is only
   one row of books, so the shelf is a cabinet rather than a strip. */
.sb-shelf {
  width: 100%;
  min-height: 100%;
  margin: auto;
  box-sizing: border-box;
  padding: 34px 0 10px;
  display: flex;
  flex-direction: column;
  /* The board under each row bleeds past the grid to the full width of the
     case, and this is what stops it bleeding past the case as well. */
  overflow: hidden;
  background:
    /* THE CASE HAS TO FALL AWAY, or a shelf holding one row of books is a plank
       on a very large brown wall — which is exactly what the first build of
       this looked like. Two gradients do it: a pool of light around the middle,
       where the books are, and a hard drop into the dark at the top and bottom
       edges. The rest of the case is then something the eye passes over on its
       way to the shelf, rather than the largest thing in the frame. */
    linear-gradient(180deg,
      rgba(0, 0, 0, .55) 0%, rgba(0, 0, 0, 0) 26%,
      rgba(0, 0, 0, 0) 72%, rgba(0, 0, 0, .5) 100%),
    /* the light in the room, falling in from the upper left */
    radial-gradient(86% 62% at 34% 44%, rgba(255, 226, 178, .13), rgba(255, 226, 178, 0) 74%),
    /* the fine vertical grain of the back panel */
    repeating-linear-gradient(90deg,
      rgba(0, 0, 0, .13) 0 1px, rgba(255, 224, 176, .035) 1px 2px, transparent 2px 9px),
    /* the case itself, darkening into its corners */
    radial-gradient(150% 118% at 50% 40%, var(--sb-case) 0%, var(--sb-case-deep) 100%);
}

.sb-shelf-head {
  font-family: 'Kalam', cursive;
  font-size: 12px;
  letter-spacing: .22em;
  text-transform: uppercase;
  text-align: center;
  color: var(--sb-case-ink);
  opacity: .62;
  /* THE HEAD AND THE BOOKS ARE ONE GROUP, and the two auto margins are what
     makes them one: this one pushes down and the grid's bottom margin pushes
     up, so the free space in the case splits evenly above and below the pair.
     Left at the top of the case with the grid centred beneath it, the line sat
     on its own with a hand's width of empty wall between it and the books it
     is talking about. */
  margin: auto auto 26px;
}

/* NO COLUMN GAP, AND IT IS THE BOARD THAT NEEDS IT. Each cell draws the whole
   board under its own row (see `.sb-cell::after`); the cells in a row have the
   same bottom edge, so those boards land on top of each other and read as one
   plank. A column gap would put a stripe of case between cover and cover, which
   is fine, and nothing at all under it, which is not — the boards would still
   meet, but the AIR between two books would be board where a gap would look
   like a gap. The air is the cell's own side padding instead.
   `auto-fill` rather than `auto-fit`: auto-fit collapses the empty tracks, so a
   shelf holding two books would stretch them to half the case each. */
.sb-shelf-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(230px, 1fr));
  gap: 0;
  /* `width: 100%` IS LOAD-BEARING AND WAS MISSING. `.sb-shelf` is a column
     flex container, so an auto margin on this axis makes the grid shrink to
     fit-content — and a fit-content grid of `1fr` tracks collapses every track
     to its 230px minimum. Measured: the covers came out 164px wide inside
     196px tracks on a 1920px window with 1240px of room going spare. An
     explicit cross size is what stops the auto margins doing that, and it is
     still capped and centred by the two lines under it. */
  width: 100%;
  max-width: 1500px;
  /* AUTO EXCEPT AT THE TOP, where `.sb-shelf-head` is already holding the
     other half of the centring — see its note. An auto margin is defined to
     become 0 when the free space is negative, which is what stops a shelf
     taller than the window from having its first row pushed off the top and out
     of reach of the scrollbar. */
  margin: 0 auto auto;
}

.sb-cell {
  position: relative;
  display: flex;
  align-items: flex-end;
  justify-content: center;
  /* The bottom padding is the board's thickness less a pixel, so a cover ends
     up standing ON the board's top lip rather than a hair above it. */
  padding: 30px 16px 45px;
}

/* THE BOARD THIS ROW OF BOOKS STANDS ON, drawn by every cell in the row and not
   by the row — because a CSS grid has no row element to hang it on, and the
   number of rows depends on how wide the window is. `left`/`right` take each
   cell's board across the whole case and `.sb-shelf` clips it; the copies land
   exactly on top of one another, and every stop in the gradient is opaque, so N
   of them look like one board rather than N stacked shadows — which is the
   reason the underside fades to `--sb-case` with a colour rather than with an
   alpha. It also means a row that is only half full still gets a plank all the
   way across, which is what a shelf looks like.

   100vw AND NOT 50vw, WHICH IS THE DIFFERENCE BETWEEN A PLANK AND HALF A PLANK.
   A cell can sit anywhere across the case, so half a viewport of bleed only
   reaches the far edge from a cell near the middle. Seen: a seventh book pushed
   the unfiled one onto a second row on its own, at the far left, and its plank
   stopped two thirds of the way across an otherwise empty shelf. A cell is
   always inside the viewport, so a full viewport of bleed on each side covers
   the case from any position it can take. */
.sb-cell::after {
  content: '';
  position: absolute;
  left: -100vw;
  right: -100vw;
  bottom: 0;
  height: 46px;
  z-index: 0;
  background: linear-gradient(180deg,
    /* the front top arris, catching the light */
    var(--sb-board-lip) 0, var(--sb-board-lip) 2px,
    /* the front edge of the plank, the light rolling off it */
    var(--sb-board-face) 8px,
    var(--sb-board-foot) 17px,
    /* the underside, and the shade it throws down the back panel */
    var(--sb-case-deep) 19px,
    var(--sb-case) 46px);
}

/* ---------- a cover ---------- */
.sb-cover {
  position: relative;
  z-index: 1;
  width: 100%;
  max-width: 300px;
  /* THE SPEC'S PROPORTIONS. A landscape board, 1.62 wide to 1.15 tall. */
  aspect-ratio: 1.62 / 1.15;
  box-sizing: border-box;
  /* THE RATIO ABOVE IS ONLY A PREFERRED SIZE, AND ON ITS OWN IT LOST. A box
     with `aspect-ratio` and `height: auto` still grows to hold content that
     does not fit, and the first build of this cover did: measured in Chrome at
     a 174.7px track, five of the six covers came back 135.4px tall — 1.29 : 1
     — while the sixth, the unfiled one, was 124.0px and exactly 1.4087 : 1,
     because it is the one with no emblem to make tall. Six books on a shelf at
     two different heights, and the odd one out was the one that was RIGHT.
     `min-height: 0` is what lets the ratio win, and `overflow: hidden` is what
     it wins with: a 40-character trunk name over an emblem cannot be made to
     fit at every size, and a cover that quietly changes shape is worse than
     one that clips. Everything below is sized so that the clip is not
     reached — see the note on the type. */
  min-height: 0;
  overflow: hidden;
  /* THE COVER IS ITS OWN QUERY CONTAINER, so the emblem and the two lines of
     writing are all fractions of the board they are stamped on rather than
     pixel sizes that only look right in one grid track. `cqw` here is 1% of
     this element's CONTENT box, which is what makes the sums in the note above
     add up at every width the grid can hand out. */
  container-type: inline-size;
  /* FLEX AND NOT A THREE-ROW GRID, because one of the books has no emblem. In
     a `1fr auto auto` grid the title of a book with no emblem climbs into the
     row the emblem would have had and the bottom row is left empty, so the
     unfiled cover's writing sits visibly higher than everybody else's. A
     centred column has nothing to leave empty: whatever is there is the group,
     and the group is in the middle. */
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  /* Percentages, so the margins hold their proportion at every cover width the
     grid can hand out. The left inset is the wider one: the spine is over
     there and nothing is set into it. */
  padding: 7% 11% 6% 15%;
  border: 0;
  /* Tight at the spine, rounded at the fore edge — which is how a bound board
     is actually cut. */
  border-radius: 2px 6px 6px 2px;
  cursor: pointer;
  font: inherit;
  text-align: center;
  color: var(--sb-foil);
  background-color: var(--sb-leather);
  background-image:
    /* the key light from the upper left, falling off downward */
    linear-gradient(172deg,
      rgba(255, 240, 214, .17) 0%,
      rgba(255, 240, 214, .05) 22%,
      transparent 46%,
      rgba(0, 0, 0, .30) 100%),
    /* THE GRAIN IS .trunk-lid's, three passes at 3/5/8px so that no two beat
       into a regular weave and turn hide into fabric. Copied and not shared:
       trunks.css declares --grain on `.trunk`, and this element is not one. */
    radial-gradient(ellipse 34% 42% at 17% 72%, rgba(255, 240, 212, .07) 0%, transparent 72%),
    radial-gradient(ellipse 26% 34% at 84% 28%, rgba(0, 0, 0, .13) 0%, transparent 72%),
    radial-gradient(ellipse 22% 30% at 62% 88%, rgba(0, 0, 0, .10) 0%, transparent 74%),
    repeating-linear-gradient(74deg, rgba(255, 244, 224, .05) 0 1px, transparent 1px 3px),
    repeating-linear-gradient(-16deg, rgba(0, 0, 0, .07) 0 1px, transparent 1px 5px),
    repeating-linear-gradient(41deg, rgba(0, 0, 0, .045) 0 1px, transparent 1px 8px),
    /* the edge vignette — what makes it read as a solid rather than a card */
    radial-gradient(ellipse 112% 106% at 44% 34%, transparent 32%, rgba(0, 0, 0, .48) 100%),
    /* and the hide's own ramp, the trunk's two stops */
    linear-gradient(180deg, var(--sb-leather) 0%, var(--sb-leather-deep) 100%);
  box-shadow:
    inset 0 0 0 1px rgba(0, 0, 0, .42),
    inset 0 1px 0 rgba(255, 238, 210, .16),
    /* THE FOIL AS AN EDGE: the gilt on the bottom board, 2px of it. */
    inset 0 -2px 0 var(--sb-foil),
    /* the book's own weight on the plank */
    0 2px 2px rgba(0, 0, 0, .45),
    0 10px 16px -7px rgba(0, 0, 0, .6);
  transition: transform .16s ease, box-shadow .16s ease;
}

/* The spine, and the hinge groove the boards fold on. */
.sb-cover::before {
  content: '';
  position: absolute;
  left: 0;
  top: 0;
  bottom: 0;
  width: 9%;
  border-radius: 2px 0 0 2px;
  background:
    linear-gradient(90deg,
      rgba(255, 238, 210, .10) 0 1px,
      rgba(0, 0, 0, .26) 12%,
      rgba(0, 0, 0, .05) 62%,
      rgba(0, 0, 0, .34) 92%,
      rgba(255, 238, 210, .07) 100%);
}

/* The blind fillet: the rule a binder runs round a board, in the same foil as
   everything else stamped on it. `inset` in percentages for the reason the
   padding is. */
.sb-cover::after {
  content: '';
  position: absolute;
  inset: 5.5% 5% 5.5% 13%;
  border: 1px solid var(--sb-foil);
  border-radius: 2px;
  opacity: .42;
  pointer-events: none;
}

.sb-cover:hover,
.sb-cover:focus-visible {
  transform: translateY(-3px);
  box-shadow:
    inset 0 0 0 1px rgba(0, 0, 0, .42),
    inset 0 1px 0 rgba(255, 238, 210, .22),
    inset 0 -2px 0 var(--sb-foil),
    0 3px 3px rgba(0, 0, 0, .4),
    0 16px 22px -9px rgba(0, 0, 0, .62);
}
.sb-cover:active { transform: translateY(-1px); }

/* THE EMBLEM IS A MASK AND THE FOIL IS WHAT IS SEEN THROUGH IT. The files in
   emblems/ are line art in `currentColor`; used as an image they would paint in
   the browser's default ink, which is black line art on leather. As a mask they
   contribute only their alpha, and the ramp on `::after` — the foil with a
   highlight raked across it — is what fills the shape. That ramp is why the
   foil is a gradient and not the flat `--sb-foil`: a stamp is metal pressed
   into a board, and metal has a light side.
 *
 * TWO PSEUDO-ELEMENTS AND NOT ONE ELEMENT WITH A `drop-shadow`. A mask on the
 * box would mask its children too, so an offset dark copy inside it comes out
 * with no offset at all; the alternatives are a filter or two independently
 * masked layers, and this block promised at the top of the file that it adds no
 * filter to the paint the book already pays for. So `::before` is the shade the
 * stamp presses into the board and `::after` is the metal sitting in it. WHAT
 * IT BUYS is visible on exactly two of the seven hides: brass foil on
 * hufflepuff and on tan is a 1.62 : 1 and a 1.37 : 1 contrast against the
 * leather under it, and without a shade the owl on the hufflepuff book was a
 * ghost. See the report — that ratio is the model's to fix, not this file's.
 *
 * THE FALLBACK MASK IS A TRANSPARENT GRADIENT, and it is not decoration:
 * `mask-image: none` means NO MASK, so a node that reached this rule without
 * `--sb-emblem` would paint as a solid foil rectangle. A gradient from
 * transparent to transparent is a real mask that lets nothing through.
 * renderCover does not append the node at all unless it painted something, so
 * this is the second line rather than the first. */
.sb-cover-emblem {
  position: relative;
  /* THE ONE THING ON THE COVER ALLOWED TO GIVE WAY. A long name takes a second
     line and there is not room for both at full size; `flex: 0 1 auto` with
     `min-height: 0` lets the crest lose height first, and because the mask is
     sized `contain` it simply gets smaller and stays centred rather than
     squashing. Without it the second line is what gets clipped, which is the
     half a reader actually needs. */
  flex: 0 1 auto;
  width: 38cqw;
  height: 38cqw;
  min-height: 0;
  margin-bottom: 4cqw;
}
.sb-cover-emblem::before,
.sb-cover-emblem::after {
  content: '';
  position: absolute;
  inset: 0;
  -webkit-mask: var(--sb-emblem, linear-gradient(transparent, transparent)) center / contain no-repeat;
  mask: var(--sb-emblem, linear-gradient(transparent, transparent)) center / contain no-repeat;
}
.sb-cover-emblem::before {
  transform: translateY(.9cqw);
  background: rgba(0, 0, 0, .5);
}
.sb-cover-emblem::after {
  background:
    linear-gradient(151deg,
      rgba(255, 255, 255, .55) 0%,
      rgba(255, 255, 255, 0) 26%,
      rgba(0, 0, 0, .26) 54%,
      rgba(255, 255, 255, .38) 78%,
      rgba(0, 0, 0, .18) 100%),
    var(--sb-foil);
}
/* A DRAWN EMBLEM IS NOT FOIL. It carries its own colours — "full colour,
   exactly as drawn", the same product decision trunks.js's `is-drawn` hangs off
   on the desk — so both stamped layers come off entirely and the SVG is drawn
   into the box as it is. `content: none` and not a transparent background:
   these two carry no mask when there is no `--sb-emblem`, and a node that
   exists to be masked and is not is a node that should not exist. */
.sb-cover-emblem.sb-drawn::before,
.sb-cover-emblem.sb-drawn::after { content: none; }
.sb-cover-emblem.sb-drawn > svg { display: block; width: 100%; height: 100%; }

.sb-cover-title {
  flex: none;
  font-family: 'Caveat', cursive;
  font-weight: 600;
  /* THE SUM THE `overflow: hidden` ABOVE IS THERE TO CATCH, written out so the
     next person changing one of these numbers knows what it is spending. As
     fractions of the cover's width W, with the ratio making the box 0.7099W
     tall and the padding taking 0.13W of that: the crest is 0.281W plus 0.030W
     of air, a line of this type is 0.117W, and the count under it is 0.092W.
     One line of title comes to 0.519W against the 0.580W there is, so it fits
     with room to spare; two lines come to 0.636W, which is where the crest
     starts giving up height and, past that, where the clip lands. */
  font-size: 15cqw;
  line-height: 1.05;
  /* Trunk names run to 40 characters and a cover is not that wide. Same clamp
     the captions in the book use. */
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  overflow: hidden;
  overflow-wrap: break-word;
  /* The stamp is pressed INTO the board, so the light catches its upper edge
     and the shade sits under it. Two shadows, not a filter. */
  text-shadow: 0 1px 0 rgba(0, 0, 0, .55), 0 -1px 0 rgba(255, 255, 255, .10);
}

.sb-cover-count {
  flex: none;
  margin-top: 3cqw;
  font-family: 'Kalam', cursive;
  font-size: 7cqw;
  line-height: 1.35;
  letter-spacing: .16em;
  text-transform: uppercase;
  /* WAS .62, AND IT IS THE QUIETEST TEXT ON THE SHELF BY SOME WAY. Modelled
     against the nine layers `.sb-cover` paints — the hide's ramp, the 172deg
     key light, the vignette — at the point this line actually sits, which the
     sums in `.sb-cover-title` above give: the group starts 0.100W down, so the
     middle of this line box is 0.585W down a 0.710W board, 82% of the way to
     the bottom. In BRASS foil (#b08d57, the default hardware and the darker of
     the two) it composites to between 2.4 and 3.0 : 1 there at .62 on all
     seven hides, tan lowest and black highest. Unlike `.sb-cover-title` above
     it, this line carries no text-shadow, so nothing gives the eye an edge
     that does not depend on that ratio.

     .82 IS A MIDDLE AND NOT A CEILING. It buys back most of the alpha while
     keeping the hierarchy the count is supposed to have — the title is at full
     strength and this is still the quieter of the two — and it lands between
     3.1 and 4.4 : 1. So AT THIS OPACITY IT DOES NOT REACH AA on any hide: at
     7cqw this is small text, where the threshold is 4.5 : 1. What it is NOT is
     unreachable. Brass at full strength clears 4.5 outright on five of the
     seven — oxblood, black, gryffindor, slytherin and ravenclaw all land
     between 5.5 and 5.9 : 1 — and going there is a decision about hierarchy,
     not a wall. The two LIGHT hides are where it stops: tan tops out near 3.9
     and hufflepuff near 4.3, so on those two — and only those two — even full
     strength is short of AA. Closing it there needs the title's text-shadow
     here as well, or a darker foil, and both are changes somebody should look
     at on a screen first. Silver foil (#c8ccd0) is not part of the problem: it is
     over 5.5 : 1 on every hide at .82 already.

     THE NUMBERS ABOVE ARE MODELLED FROM THE LAYER STACK AND NOT SAMPLED — no
     page was rendered to get them, and the three grain passes are taken at
     their area average because they are 1px stripes in a 3/5/8px period. On
     the five dark hides that approximation moves nothing: they clear AA with
     room under every variant of it. Tan does not clear under any. Hufflepuff
     is the one sitting close enough to 4.5 that the variants straddle it, so
     read its number as NOT DEMONSTRATED rather than as shown to fail. */
  opacity: .82;
  white-space: nowrap;
}

/* ---------- the book that is not a trunk ----------
 *
 * THE ODD ONE OUT, ON PURPOSE. The unfiled book holds the photos that are on
 * the desk rather than in any trunk, so it is not bound in anybody's leather:
 * SketchbookModel gives it parchment and no emblem, and this makes the rest of
 * it agree — paper boards with a ruled label where a stamped board would have a
 * fillet and a crest, ink instead of foil, and a matt edge instead of gilt. It
 * still stands on the same plank, at the same size, with the same weight of
 * shadow under it, so it reads as a book on the shelf that is simply a
 * different sort of book rather than as one that failed to load. */
.sb-cover.sb-loose {
  --sb-ink-loose: #4a3a22;
  color: var(--sb-ink-loose);
  background-color: var(--sb-leather);
  background-image:
    /* A GENTLER WASH THAN THE FIRST BUILD'S .5, WHICH WAS TOO MUCH. It lifted
       the parchment well above SketchbookModel's #d8cbb0 and made this the
       brightest thing in the case by a distance — so the first book the eye
       landed on was the one that is deliberately not like the others. It should
       read as different at a glance, not first. */
    linear-gradient(172deg, rgba(255, 252, 244, .26) 0%, transparent 44%,
      rgba(112, 88, 48, .2) 100%),
    /* the tooth of the paper, at a period nothing else here uses */
    repeating-linear-gradient(63deg, rgba(120, 96, 56, .05) 0 1px, transparent 1px 4px),
    repeating-linear-gradient(-27deg, rgba(120, 96, 56, .04) 0 1px, transparent 1px 7px),
    radial-gradient(ellipse 112% 106% at 44% 34%, transparent 40%, rgba(108, 84, 46, .3) 100%),
    linear-gradient(180deg, var(--sb-leather) 0%, var(--sb-leather-deep) 100%);
  box-shadow:
    inset 0 0 0 1px rgba(90, 68, 34, .45),
    inset 0 1px 0 rgba(255, 255, 255, .5),
    inset 0 -2px 0 rgba(122, 98, 58, .55),
    0 2px 2px rgba(0, 0, 0, .45),
    0 10px 16px -7px rgba(0, 0, 0, .6);
}
.sb-cover.sb-loose::before {
  /* No spine of its own: this is a paper wrapper, not a bound board. What is
     left is the fold where it turns over. */
  background: linear-gradient(90deg,
    rgba(120, 96, 56, .22) 0 1px, rgba(120, 96, 56, .05) 40%, transparent 100%);
}
.sb-cover.sb-loose::after {
  /* A ruled label rather than a stamped fillet: two lines, top and bottom, the
     way a label is written between the rules on a copybook. */
  inset: 22% 12% 24% 18%;
  border: 0;
  border-top: 1px solid rgba(90, 68, 34, .34);
  border-bottom: 1px solid rgba(90, 68, 34, .34);
  border-radius: 0;
  opacity: 1;
}
.sb-cover.sb-loose:hover,
.sb-cover.sb-loose:focus-visible {
  box-shadow:
    inset 0 0 0 1px rgba(90, 68, 34, .45),
    inset 0 1px 0 rgba(255, 255, 255, .6),
    inset 0 -2px 0 rgba(122, 98, 58, .55),
    0 3px 3px rgba(0, 0, 0, .4),
    0 16px 22px -9px rgba(0, 0, 0, .62);
}
.sb-cover.sb-loose .sb-cover-title {
  text-shadow: 0 1px 0 rgba(255, 255, 255, .55);
}

/* ---------- the book's frame ---------- */
.sb-wrap {
  display: grid;
  justify-items: center;
  /* A BARE 14px AND NO LONGER A NAMED `--sb-row-gap`. It was named because one
     row did arithmetic with it — the rail of ribbons cancelled exactly this gap
     with a negative bottom margin before it could start eating into the book's
     row. The rail is not a row of this grid any more (see below), nothing
     subtracts this from anything, and a variable with one reader is a name that
     only looks like it is holding two numbers together. */
  gap: 14px;
  /* THE THREE LENGTHS OF THE RAIL, DECLARED HERE AND NOT ON `.sb-rail`. The
     rail hangs inside the book's own transform now, two rows down, and the room
     it used to take as a row of this grid is kept by the margin on `.sb-stage`
     — which is `--sb-mark-h-on` minus `--sb-tuck`, exactly. The rail and the
     stage are not ancestors of one another, so the pair has to be declared on
     the box that is an ancestor of both, and this is it. Written out as bare
     pixels in the two places instead, the day somebody makes a ribbon taller is
     the day the ribbons start sitting on the close button.

     --sb-tuck is how deep the paper covers the ribbons. It is also how far the
     numerals sit above that line — see the padding on `.sb-mark`. */
  --sb-tuck: 10px;
  --sb-mark-h: 36px;
  --sb-mark-h-on: 46px;
  /* HOW WIDE THE OPEN BOOK IS, declared here rather than on `.sb-3d` because
     this is the box the rest of the frame's lengths are declared on and one
     place is the whole point of them; `.sb-3d` reads it, and the rail takes the
     same width from its containing block without naming it at all. The argument
     for the three terms is over `.sb-3d`, where the width is used; the
     narrow-screen override at the foot of this file re-declares this line. */
  --sb-book-w: min(1360px, 94vw, calc(74vh * 2000 / 710));
  width: 100%;
  padding: 18px 12px;
}

/* ---------- the bookmark ribbons ----------
 *
 * ONE RIBBON PER PAGE, hanging out of the top of the open book, with the two
 * pages lying open marked and the rest of the book a tap away.
 *
 * INSIDE THE BOOK'S OWN TRANSFORM, WHICH IS THE WHOLE OF HOW THEY STAY ATTACHED
 * TO IT. `.sb-tilt` leans the book toward the pointer by up to 4.5 and 7 degrees
 * — writeView() in sketchbook.js writes the two angles onto `.sb-3d` and that
 * element reads them. This rail used to be a ROW of `.sb-wrap`'s grid, a sibling
 * of `.sb-stage`, outside the transform and inheriting nothing from it: a book
 * that leaned pulled its own top edge out from under ribbons that stayed square,
 * and the two came visibly apart. Seen on screen, which is why this is a
 * positioned child of `.sb-tilt` now. Everything in that element leans together
 * because it is ONE transform and not four things agreeing to the same angle.
 *
 * WHICH TURNS THE TUCK FROM A MARGIN INTO AN OFFSET. `bottom` of
 * `calc(100% - var(--sb-tuck))` hangs the rail off the top edge of `.sb-tilt` —
 * whose box IS the book's, the casts being absolute and this rail with them —
 * leaving exactly --sb-tuck of ribbon below that edge. `left` and `right` of 0
 * give the rail the book's width without asking for it: the cap this rule used
 * to carry (`max-width: var(--sb-book-w)`, so that a rail running past the paper
 * could not leave a ribbon or two at each end with nothing to tuck into) is the
 * containing block now, and a second copy of a width here would be a copy that
 * can disagree with the book it belongs to.
 *
 * AND WHAT COVERS THE TAILS IS THE PAINT ORDER INSIDE `.sb-tilt`, not the one
 * outside it. `.sb-stage` being positioned and `.sb-rail` not, which is what
 * used to do this job, now decides nothing at all: both are inside the same
 * transform. What does it is that this rail is positioned at `z-index: auto` and
 * `.sb-book` carries `z-index: 1`, so the paper is painted in a later step than
 * the ribbons whichever way round the two are written — CSS 2.1's step 7 against
 * its step 6 — and the strip of ribbon below the book's top edge simply goes
 * behind it. There is one deliberate exception and it is written out over
 * `.sb-mark.is-dragging`.
 *
 * THE ROOM IT USED TO TAKE IN THE GRID IS KEPT BY `.sb-stage`'s margin-top, out
 * of the same two lengths on `.sb-wrap`. Without it the book climbs 36px up the
 * page and the ribbons go over the close button.
 *
 * WHAT TO DO IF IT IS WRONG ON SCREEN: take `--sb-tuck` to 0. The ribbons then
 * sit flush on the book's top edge, which is the shape this is drawn in anyway
 * — the fork is at the FREE end, at the top, so a square bottom against the
 * paper reads as a ribbon going into the book either way.
 */
.sb-rail {
  position: absolute;
  /* The book's width, taken from the containing block rather than named. */
  left: 0;
  right: 0;
  /* The rail's bottom edge, --sb-tuck below the top edge of the book. 100% is
     the height of `.sb-tilt`, which is the height of the book. */
  bottom: calc(100% - var(--sb-tuck));
  /* A book holds every unfiled photo on the desk — around fifty-five of them,
     and every new photo lands there — so the ROW inside this box is several
     times the width of the book and this box scrolls it sideways. (The box
     itself is exactly the book's width; the two lengths above are what make it
     so.) `overflow-y` is `hidden` and not `visible`
     ON PURPOSE: a box with one axis scrollable and the other visible has the
     visible axis computed to `auto` regardless, so `visible` here would be a
     declaration that quietly means something else. Nothing needs to escape
     vertically — the ribbons are inside this box, and it is the BOX that
     overlaps the book. */
  overflow-x: auto;
  overflow-y: hidden;
  /* No bar. It would draw a grey line along the top edge of the book, and this
     rail is moved by the page turning or by dragging a ribbon off the end of
     it, never by aiming at 8px of chrome. */
  scrollbar-width: none;
  /* NO `scroll-behavior: smooth` HERE, AND IT IS NOT AN OVERSIGHT. syncRail()
     brings the open spread back on screen with two `scrollIntoView` calls, one
     per ribbon of the pair, and they only compose because each is applied
     before the next is measured. A smooth scroll is queued instead, so the
     second call would measure a rail that has not moved and supersede the
     first — leaving the right-hand page of the pair off the end of a long
     rail. The arithmetic is worked through over those two calls in
     sketchbook.js. */
}
.sb-rail::-webkit-scrollbar { display: none; }

.sb-rail-track {
  display: flex;
  /* BOTTOM ALIGNED, so every ribbon's tail meets the paper on the same line and
     the taller current one reaches further OUT of the book — the way a bookmark
     somebody has pulled at does — rather than deeper into it. */
  align-items: flex-end;
  gap: var(--sb-mark-gap);
  /* FIXED AT THE TALLEST A RIBBON GETS, whether or not that ribbon is on
     screen — and this is the rail's height as well, because `.sb-rail` is
     anchored by its BOTTOM edge and has no height of its own. A track that took
     its height from its children would grow and shrink by the 10px between
     `.sb-mark` and `.sb-mark.is-current` on every page turn, and with the bottom
     edge pinned to the paper what that moves is the TOP of the rail: fifty-five
     ribbons growing and shrinking together on every turn. (When this rail was a
     row of the book's own grid, the same 10px moved the BOOK. Different victim,
     same line.) */
  height: var(--sb-mark-h-on);
  /* As wide as the ribbons really are, so that the 12px at the far end is at
     the far end: a block box inside a scroll container is only as wide as the
     container, and the end padding would sit at the edge of the CLIP rather
     than after the last ribbon. */
  width: max-content;
  padding: 0 12px;
}

.sb-mark {
  flex: none;
  /* THE WIDTH AND THE GAP ARE WRITTEN ONTO `.sb-rail` BY sketchbook.js, from
     MARK_W and MARK_GAP, and there is deliberately NO FALLBACK on either. The
     same two numbers are what SketchbookRail.insertionIndex turns a pointer
     position into a ribbon index with, so a rail drawn at a pitch the drop
     arithmetic does not share drops a ribbon one place from where it was let
     go — further out the further along the rail you are, with nothing on screen
     to say so. A fallback here would be a second number that agrees until
     somebody edits one of them. What a rail that never got them looks like:
     `width` falls back to its initial `auto` and the gap to `normal`, which is
     0 across a flex row, so the ribbons come out as wide as their numerals and
     butted together — wrong at a glance, rather than a plausible rail at a
     pitch the arithmetic disagrees with. */
  width: var(--sb-mark-w);
  height: var(--sb-mark-h);
  /* The numeral sits clear of the paper's edge rather than centred in a ribbon
     whose bottom is behind the book. */
  display: flex;
  align-items: flex-end;
  justify-content: center;
  padding-bottom: calc(var(--sb-tuck) + 5px);
  border: 0;
  /* THE FORK IS AT THE TOP, WHICH IS THE FREE END — not at the bottom, where
     the plan drew it. A swallowtail is the cut end of a ribbon, and the cut end
     of this one is the end sticking out of the book; put it at the bottom and
     the one piece of shape this thing has is the piece the paper covers. */
  clip-path: polygon(0 0, 50% 22%, 100% 0, 100% 100%, 0 100%);
  /* The book's own boards and its own metal, written onto the layer by open()
     from what the model resolved — and carrying no fallback for the same
     reason the two lengths above do not: the seven leathers and two foils are
     SketchbookModel's table, pinned there by a test against trunks.css, and a
     colour repeated into this file would be a copy that goes on looking
     plausible after the table beneath it has moved.
     Cloth over the leather: a wash down the length for the fall of the light,
     and a fine warp across it, so a ribbon is not a flat chip of colour. */
  background-color: var(--sb-leather);
  background-image:
    linear-gradient(180deg, rgba(255, 255, 255, .16) 0%, transparent 46%, rgba(0, 0, 0, .34) 100%),
    repeating-linear-gradient(90deg, rgba(0, 0, 0, .10) 0 1px, transparent 1px 3px);
  color: var(--sb-foil);
  font-family: 'Kalam', cursive;
  font-size: 11px;
  line-height: 1;
  letter-spacing: .02em;
  overflow: hidden;
  /* GRAB NOW, because a ribbon really can be picked up and carried: a press
     that travels more than 4px starts a reorder. The `grabbing` for the ribbon
     in hand is on `.is-dragging` below. A tap still jumps to the page, so this
     is a cursor that under-promises rather than one that lies — there is no
     third cursor for "either of two things". */
  cursor: grab;
  /* THE HORIZONTAL AXIS IS OURS, THE VERTICAL IS THE PAGE'S. Without this a
     touch drag along a ribbon is taken by `.sb-rail`'s own `overflow-x: auto`
     and scrolls the rail instead of moving the ribbon — the browser claims the
     gesture before the first pointermove is delivered, so no amount of
     preventDefault in the handler can get it back. `pan-y` and not `none`: the
     page behind the book still scrolls under a finger that started on a ribbon,
     which is what somebody swiping down a phone expects. */
  touch-action: pan-y;
  -webkit-tap-highlight-color: transparent;
  transition: height .16s ease, filter .16s ease;
}
/* A ribbon leans out of the book a little under the cursor. Height rather than
   a transform, so it grows from the bottom edge it shares with the paper. The
   `:not()` is what keeps the open spread's pair at their full height instead of
   shrinking by 5px when the cursor lands on one — `.sb-mark.is-current` below
   would win that on source order alone, which is not a thing to rely on. */
.sb-mark:hover:not(.is-current) { height: calc(var(--sb-mark-h) + 5px); filter: brightness(1.12); }
/* BOTH RIBBONS OF THE OPEN SPREAD, because a spread is two pages lying open and
   marking one of them would point at a page no more open than its neighbour.
   Taller, so the pair reads as two ribbons pulled out of the book, and brighter
   along the top where they catch the light. */
.sb-mark.is-current {
  height: var(--sb-mark-h-on);
  background-image:
    linear-gradient(180deg, rgba(255, 255, 255, .40) 0%, rgba(255, 255, 255, .08) 40%, rgba(0, 0, 0, .26) 100%),
    repeating-linear-gradient(90deg, rgba(255, 255, 255, .07) 0 1px, transparent 1px 3px);
  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, .22);
}

/* THE FOCUS RING IS AN INSET SHADOW AND NOT AN `outline`, and that is not a
   matter of taste: `clip-path` clips everything the element paints, an outline
   included, and the polygon on `.sb-mark` runs to the border box — so an
   outline at any offset is clipped away entirely and tabbing to a ribbon would
   land on it with nothing at all to show for it. These are buttons on a rail of
   fifty-five, and a keyboard has to be able to see which one it is on. An inset
   shadow paints inside the clip and survives it.
   TWO RINGS, light inside dark, because the same ribbon is dark leather in one
   book and pale parchment in the next and either colour alone disappears
   against one of them.
   WRITTEN AFTER `.is-current`, which is the tie it has to win: both selectors
   are a class plus one more, so source order decides, and a focused ribbon
   showing the ordinary hairline instead would be the whole point lost. The
   pair's extra height is still there to say which is which. The UA's own
   outline is left alone rather than switched off — it is clipped here, but a
   forced-colours mode that draws focus its own way should keep it. */
.sb-mark:focus-visible {
  box-shadow:
    inset 0 0 0 2px rgba(255, 255, 255, .95),
    inset 0 0 0 4px rgba(38, 26, 14, .8);
}

/* The parchment book's ribbons are paper tabs, inked rather than foiled — the
   same argument `.sb-cover.sb-loose` makes for the book it belongs to. The ink
   is a SECOND COPY of the `--sb-ink-loose` declared up there, said out loud
   because that one is scoped to the cover and the rail is not inside it. What
   it buys: the foil this would otherwise use is brass #b08d57, and brass on
   #d8cbb0 parchment is 1.9 : 1, at 11px.

   THE CLASS IS ON `.sb-wrap` AND NOT ON `.sb-rail`, which is where it used to
   be. Two things wear this ink — the ribbons and the preview card's caption —
   and the rail has moved inside the book's transform while the card has stayed
   a child of the wrap, so there is no longer any combinator that reaches from
   one to the other. `.sb-rail.sb-loose ~ .sb-mark-preview` was the rule that
   did, and a sibling selector whose two sides are no longer siblings matches
   nothing and says nothing. One class, on the one box that is an ancestor of
   both; sketchbook.js puts it there. */
.sb-wrap.sb-loose .sb-mark { color: #4a3a22; }
.sb-wrap.sb-loose .sb-mark.is-current { color: #32270f; }

/* THE RIBBON IN HAND. Lifted off the rail rather than merely tinted, so that the
   row shuffling around it reads as the row moving and not as the colours
   changing — this is the one element on the rail whose position is not where the
   layout put it.
   NO TRANSITION ON THE TRANSFORM, and that is the point of overriding the
   `transition` `.sb-mark` sets: the ribbons around it slide because the flex row
   is re-laid-out under them, which is instant, and a dragged ribbon easing into
   each new height on the way would lag its own row.
   THE z-index IS THE ONE EXCEPTION THE NOTE AT THE TOP OF THIS BLOCK PROMISES:
   the rail as a whole paints under the paper, and this one ribbon paints over
   it. The mechanism, stated plainly rather than hinted at. `.sb-rail` is
   positioned now but at `z-index: auto`, which makes no stacking context, so a
   ribbon with a z-index of its own is not trapped inside the rail — it competes
   in the nearest stacking context above, which is `.sb-tilt` (it carries a
   transform). What it competes against there is `.sb-book`'s own `z-index: 1`.
   A tie, and a tie is broken by source order, so sketchbook.js appends the rail
   AFTER the book inside the tilt for exactly this reason and says so.

   What the lift is worth is small and bounded, because `.sb-rail` still clips:
   it is the --sb-tuck of ribbon that is normally behind the book's top edge,
   which is exactly the part a ribbon being carried should not disappear into.

   AND IF IT EVER STOPS WORKING IT WILL NOT BE THE z-index. `.sb-tilt` is
   `transform-style: preserve-3d` and this rail has an `overflow`, which forces
   the rail's own transform-style to flat — its descendants are flattened into
   the rail's plane before that plane is composited. A browser that reads that
   as licence to composite the rail as one unit takes this ribbon down with the
   rest of the rail and the tuck of it disappears behind the paper while it is
   being carried. Nothing else depends on this line: the ribbon is still lifted
   clear of its NEIGHBOURS by the same z-index, which is what keeps its shadow
   off the ribbons sliding past it. */
.sb-mark.is-dragging {
  cursor: grabbing;
  position: relative;
  z-index: 1;
  transition: none;
  filter: brightness(1.2);
  box-shadow: 0 6px 14px rgba(0, 0, 0, .45);
}
/* The whole row while a ribbon is in hand, so the cursor does not flick back to
   `grab` every time the pointer crosses a gap between two ribbons — or lands on
   a neighbour that has just slid out from under it. The class is put on the
   track by sketchbook.js at the same moment it goes on the ribbon; `:has()`
   would say the same thing without the second class, and is not used because
   this file supports whatever browser the owner's phone is running and a cursor
   is not worth finding out. */
.sb-rail-track.is-dragging,
.sb-rail-track.is-dragging .sb-mark { cursor: grabbing; }

/* ---------- the card that says which photo a ribbon is ----------
 *
 * A ribbon is 30px of leather with a page number on it, and the question
 * somebody dragging one is actually asking is WHICH PHOTO IS THIS. So the card
 * carries the photo and its caption, on hover and for the whole of a drag.
 *
 * `position: fixed` AND NOT absolute, and the reason is the rail rather than
 * taste: `.sb-rail` is the scroll clip (`overflow-x: auto`, `overflow-y:
 * hidden`), so a card inside it would be cut off at exactly the edge it has to
 * hang over. Out of the rail it needs a containing block, and the only
 * positioned box anywhere near is `.sb-stage` — which is the BOOK, and the book
 * LEANS: `.sb-tilt` is inside it, so a card positioned against the stage would
 * swing with the paper, and the rail is now inside that same transform. Fixed
 * needs no containing block at all, and this card is a child of `.sb-wrap`
 * rather than of the rail it describes precisely so that nothing between it and
 * the viewport carries a transform, a filter or a will-change — `.sb-tilt`'s
 * transform is one row down, inside `.sb-stage`, with the rail under it.
 *
 * WHICH MAKES sketchbook.js RESPONSIBLE FOR BOTH COORDINATES. They are written
 * as inline left/top from the ribbon's box and the rail's, on every hover and
 * every frame of a drag — see placePreview. There is deliberately no `left` or
 * `top` here: a default would be a position the card sat at for one frame
 * before the first placement, in the top-left corner of the window.
 *
 * pointer-events: none IS LOAD-BEARING, not hygiene. The card hangs directly
 * over the rail, and a drag carries the pointer under it constantly; a card
 * that hit-tested would take the pointermove that was moving the ribbon and the
 * pointerup that was dropping it. */
.sb-mark-preview {
  position: fixed;
  z-index: 90;              /* over the magnifier's 80, which is the tallest thing in the book */
  pointer-events: none;
  display: flex;
  flex-direction: column;
  gap: 6px;
  width: 168px;
  padding: 8px;
  border-radius: 4px;
  /* The book's own boards, so the card reads as part of the same object as the
     ribbon it came off — and inheriting rather than repeating a colour, for the
     reason `.sb-mark` gives above. */
  background: var(--sb-leather);
  box-shadow: 0 10px 26px rgba(0, 0, 0, .5), inset 0 0 0 1px rgba(255, 255, 255, .14);
}
/* `display: flex` above BEATS the user agent's own `[hidden]` rule, so without
   this line hiding the card would do nothing at all. Same trap, same line, as
   `.sketchbook-layer[hidden]` and the corner pill. */
.sb-mark-preview[hidden] { display: none !important; }
.sb-mark-preview-shot {
  display: block;
  width: 100%;
  /* A FIXED BOX AND object-fit, so the card is the same size for a portrait
     photo and a landscape one. A card that changed height with its photo would
     move its own top edge on every ribbon the pointer crossed, because the top
     is measured up from the rail. */
  height: 108px;
  object-fit: cover;
  border-radius: 2px;
  background: rgba(0, 0, 0, .25);
}
.sb-mark-preview-cap {
  color: var(--sb-foil);
  font-family: 'Kalam', cursive;
  font-size: 12px;
  line-height: 1.25;
  /* Two lines at most: this is a label on a card that follows a pointer, not the
     caption on the page, and a note-length caption would grow the card past the
     top of the window. `-webkit-line-clamp` with its `display: -webkit-box` is
     the same pairing `.sb-cap` uses on the page itself. */
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
/* Ink and not foil, the same swap `.sb-wrap.sb-loose .sb-mark` makes and for the
   same measured reason: brass #b08d57 on #d8cbb0 parchment is 1.9 : 1. */
.sb-wrap.sb-loose .sb-mark-preview-cap { color: #4a3a22; }

.sb-stage {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  position: relative;
  /* THE ROOM THE RIBBONS STAND IN. They hang above the book from inside
     `.sb-tilt` — the block over `.sb-rail` is the argument — so they are out of
     this grid and take no space in it; this is the space they would have taken.
     --sb-mark-h-on is how tall a ribbon at its tallest is and --sb-tuck is how
     much of that is behind the paper, so the difference is exactly how far the
     rail reaches above the book's top edge. Delete this line and the book climbs
     36px and the ribbons are drawn over the close button. */
  margin-top: calc(var(--sb-mark-h-on) - var(--sb-tuck));
  /* Vertical scrolling still belongs to the page; horizontal drags are ours. */
  touch-action: pan-y;
}

.sb-arrow {
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 6px 10px;
  border: 0;
  background: transparent;
  color: rgba(255, 255, 255, .42);
  font: inherit;
  font-size: 26px;
  line-height: 1;
  cursor: pointer;
  transition: color .2s;
  -webkit-tap-highlight-color: transparent;
  z-index: 8;
}
.sb-arrow:hover { color: rgba(255, 255, 255, .9); }
.sb-arrow:disabled { opacity: .22; cursor: default; }

/* The box the book stands in. The perspective lives here and nothing else
   does, so the leaf lifts toward the reader instead of shearing flat.
 *
 * THE SIZE IS BOUNDED ON BOTH AXES. A spread is 2000 x 710 page units (see
 * sketchbook-layout.js — one landscape page is 1000 x 710 and the book opens to
 * two of them), which is 2.82 : 1. Widening to the viewport alone would put the
 * bottom of the book below the fold on any laptop, so the height term caps it.
 *
 * The expression itself is `--sb-book-w`, declared on `.sb-wrap` with the rest
 * of this frame's lengths and read here. The rail of ribbons above the book is
 * this wide too, and takes it from its containing block rather than from a
 * second copy of a three-term min() that could drift away from this one. */
.sb-3d {
  position: relative;
  width: var(--sb-book-w);
  /* THE REFERENCE'S PERSPECTIVE IS 1750px AND ITS BOOK IS AT MOST 900px WIDE,
     and the two are one number: perspective is a distance from the viewer, so
     it only means anything relative to the size of the thing it is looking at.
     Ours is 1360px across, and left at a flat 1750px the lifted leaf came out
     of the book at 1.5x and covered the whole window — seen, before this line
     existed. 1.944 is 1750 / 900, so the leaf now stands in exactly the
     proportion the reference tuned it to, at any width the book is given. */
  perspective: calc(var(--bw, 1360px) * 1.9444);
  perspective-origin: 50% 46%;
}

/* THE LEAN AND THE ZOOM LIVE ON THEIR OWN ELEMENT, between the box that holds
   the perspective and the book itself, exactly as the reference has it — and it
   is not tidiness. `.sb-3d` is the book's untransformed frame, and the glass
   and the magnified copy are absolutely positioned in it; a rotate written onto
   `.sb-3d` would take both of them round with the paper, which is the one thing
   a magnifier lying on a page must not do.

   `--rx`, `--ry` and `--zoom` are written onto `.sb-3d` by writeView() and read
   here through inheritance. So are `--tt` and `--td`, which is what lets the
   COPY of a turning leaf inside `.zoominner` swing with the real one. */
.sb-tilt {
  position: relative;
  transform-style: preserve-3d;
  transform: rotateX(var(--rx, 0deg)) rotateY(var(--ry, 0deg)) scale(var(--zoom, 1));
  will-change: transform;
}

.sb-book {
  position: relative;
  width: 100%;
  aspect-ratio: 2000 / 710;
  transform-style: preserve-3d;
  z-index: 1;
  /* --bw is the whole spread's width in px, written by Sketchbook's layout()
     on every paint and on resize; the reference's strip arithmetic is all in
     terms of it. --pu is ONE PAGE UNIT in px — half the spread over the 1000
     units a page is wide — and it is what turns every number the packer
     produced into a length. Deriving it from --bw rather than setting it from
     JS separately means the two can never be a frame out of step. */
  --pu: calc(var(--bw, 0px) * 0.0005);
  --pg: 0%;
}

/* The book sits on a surface, so its shadow is a soft pool underneath — no
   box, no filter on the artwork, nothing with an edge of its own. Both blobs
   ease off while a leaf is in the air. Ported from the reference, with the
   vertical extents opened up: its book is 1.42 : 1 and ours is 2.82 : 1, so
   the same percentages would have drawn the pool inside the paper. */
.sb-cast { position: absolute; pointer-events: none; z-index: 0; }
.sb-cast.ambient {
  left: 3%; right: 3%; top: 62%; bottom: -14%;
  background: radial-gradient(50% 50% at 50% 46%,
    rgba(0, 0, 0, .40) 0%, rgba(0, 0, 0, .22) 40%, rgba(0, 0, 0, 0) 74%);
  filter: blur(26px);
  opacity: calc(1 - var(--shade, 0) * .42);
}
.sb-cast.contact {
  left: 7%; right: 7%; top: 82%; bottom: -6%;
  background: radial-gradient(50% 44% at 50% 40%,
    rgba(0, 0, 0, .46) 0%, rgba(0, 0, 0, .20) 48%, rgba(0, 0, 0, 0) 78%);
  filter: blur(11px);
  opacity: calc(1 - var(--shade, 0) * .5);
}

/* ---------- a page ----------
 *
 * A PAGE IS A FIXED 1000 x 710 GRID SCALED TO FIT (sketchbook-layout.js). Every
 * slot is positioned as a percentage of those units, so the same numbers
 * describe the same layout at every viewport size — which is what lets two
 * people share one set of saved positions.
 *
 * Absolutely positioned with an explicit width in all three of the places a
 * page appears — beside its twin in the resting spread, alone in a half while a
 * leaf is in the air, and shifted inside a strip's face — so one rule sizes it
 * everywhere and only `left` changes. */
.sb-page {
  position: absolute;
  top: 0;
  height: 100%;
  width: calc(var(--bw, 0px) * 0.5);
  color: var(--sb-ink);
  overflow: hidden;

  /* Which way the outer edge of this sheet lies. Both halves of the rule below
   * are written once against this, so the two pages of a spread grub toward
   * opposite edges instead of both darkening to the right. Overridden per side
   * two rules down; the fallback keeps a page with no side class from failing
   * to parse the gradient. */
  --sb-out: right;
  /* The fold geometry. Real values arrive from the .sb-fa-* / .sb-fp-* classes
   * further down, which sketchbook.js picks by page number; these are the
   * fallback for a page that somehow carries neither. */
  --f1a: 90.8deg; --f2a: 179.4deg;
  --f1p: 38%;     --f2p: 44%;

  /* ---------------------------------------------------------------- the sheet
   *
   * ALL OF THIS IS PAINT, NOT PIXELS. Nothing here is fetched: the two SVGs are
   * inline `data:` URIs and everything else is a gradient, because the site has
   * no CDN and adding a bitmap to the repo is the owner's call and not this
   * rule's. See the note on --sb-age for the one number that scales it.
   *
   * READ THE LIST BOTTOM-UP — the first layer listed paints LAST, on top.
   *
   *   mottle   one soft cloud field of coffee, stretched across the WHOLE
   *            spread and shown to each page by `background-position`: the left
   *            page gets its left half, the right page its right half. So the
   *            unevenness runs continuously through the gutter instead of two
   *            pages wearing the same stain — which is the mirrored-tile
   *            failure this design exists to avoid. It never repeats, because
   *            exactly one copy is stretched over the sheet.
   *   edges    old paper grubs at its edges. One gradient does both ends of the
   *            sheet: a little at the gutter, much more at the outer edge, plus
   *            a hard rim in the last 3%. A second does top and bottom.
   *   fibre    the tooth. `soft-light` against everything under it, so it
   *            lightens and darkens around the tone rather than greying it.
   *            The tile's own mean grey measures 125.8 — near enough the 127.5
   *            soft-light calls neutral that the sheet's tone survives it. The
   *            same tile composited `normal` would pull a 224-level sheet
   *            toward 126 at whatever opacity it was given, which is a tint,
   *            not a texture.
   *   folds    two fold lines, each a soft dark valley with a lit ridge beside
   *            it. Angles just off 90 and 180 so they are creases and not
   *            rules.
   *   veil     the age dial. A wash of the fresh colour over the finished aged
   *            sheet, so --sb-age: 0 leaves flat clean paper and 1 leaves all
   *            of the above.
   *
   * The drawings are NOT in this stack. Every layer here is a background of the
   * page and the prints, captions and notes are children painted above the lot,
   * so ageing the paper cannot touch her artwork however far the dial goes. */
  background-color: var(--paper);
  background-image:
    /* veil */
    linear-gradient(var(--sb-veil), var(--sb-veil)),
    /* fold 1 */
    linear-gradient(var(--f1a),
      rgba(var(--sb-stain), 0)                                calc(var(--f1p) - 1.7%),
      rgba(var(--sb-stain), calc(.085 * var(--sb-age)))       calc(var(--f1p) - .15%),
      rgba(var(--sb-stain), calc(.062 * var(--sb-age)))       calc(var(--f1p) + .15%),
      rgba(var(--sb-ridge), calc(.26 * var(--sb-age)))        calc(var(--f1p) + .55%),
      rgba(var(--sb-ridge), 0)                                calc(var(--f1p) + 2%)),
    /* fold 2 */
    linear-gradient(var(--f2a),
      rgba(var(--sb-stain), 0)                                calc(var(--f2p) - 2.2%),
      rgba(var(--sb-stain), calc(.07 * var(--sb-age)))        calc(var(--f2p) - .2%),
      rgba(var(--sb-stain), calc(.052 * var(--sb-age)))       calc(var(--f2p) + .2%),
      rgba(var(--sb-ridge), calc(.21 * var(--sb-age)))        calc(var(--f2p) + .7%),
      rgba(var(--sb-ridge), 0)                                calc(var(--f2p) + 2.6%)),
    /* fibre */
    var(--sb-fibre),
    /* edges: gutter side, then out to the outer edge */
    linear-gradient(to var(--sb-out),
      rgba(var(--sb-stain), calc(.07 * var(--sb-age))) 0%,
      rgba(var(--sb-stain), 0)                         24%,
      rgba(var(--sb-stain), calc(.045 * var(--sb-age))) 82%,
      rgba(var(--sb-stain), calc(.11 * var(--sb-age))) 97.5%,
      rgba(var(--sb-stain), calc(.19 * var(--sb-age))) 100%),
    /* edges: top and bottom */
    linear-gradient(180deg,
      rgba(var(--sb-stain), calc(.10 * var(--sb-age))) 0%,
      rgba(var(--sb-stain), 0)                         6%,
      rgba(var(--sb-stain), 0)                         94%,
      rgba(var(--sb-stain), calc(.10 * var(--sb-age))) 100%),
    /* mottle */
    var(--sb-mottle);
  background-repeat: no-repeat, no-repeat, no-repeat, repeat, no-repeat, no-repeat, no-repeat;
  background-size:
    100% 100%, 100% 100%, 100% 100%,
    200px 200px,
    100% 100%, 100% 100%,
    200% 100%;
  background-position:
    0 0, 0 0, 0 0,
    0 0,
    0 0, 0 0,
    var(--sb-mottle-x, 0%) 0;
  background-blend-mode: normal, normal, normal, soft-light, normal, normal, normal;
}
/* The outer edge is the one AWAY from the gutter, and the half of the mottle
   field a page shows is the half it occupies in the spread. */
.sb-page.left  { --sb-out: left;  --sb-mottle-x: 0%; }
.sb-page.right { --sb-out: right; --sb-mottle-x: 100%; }

/* ---------- where each page's folds fall ----------
 *
 * TWO TABLES OF DIFFERENT LENGTHS, ON PURPOSE. sketchbook.js gives every page a
 * number — spread index times two, plus one for a right-hand page — and takes
 * the angles at number % 7 and the positions at number % 5. Seven and five are
 * coprime, so the pair repeats every 35 pages rather than every 7; one table of
 * seven would have put the same creases back every three and a half spreads.
 *
 * DERIVED FROM THE PAGE NUMBER AND NOTHING ELSE, which is the whole reason it
 * is a lookup and not a random draw. Two people are reading the same book in
 * two browsers and the folds have to fall in the same places in both, and
 * survive a reload. There is no Math.random anywhere in this feature.
 *
 * Facing pages always differ: a left page is an even number and its right twin
 * is odd, so they never land on the same row of either table.
 *
 * The angles sit a degree or so off 90 and 180, so a fold drifts several pixels
 * across the sheet instead of running exactly parallel to the page edge.
 * Compared on one spread by forcing every page to 90deg/180deg and back: at
 * exactly 90 the fold is dead level with the top edge for its whole length. The
 * difference is small — this is a nudge, not a rescue — but it is the thing
 * that keeps the fold from sitting in the frame's own geometry. */
.sb-page.sb-fa-0 { --f1a: 90.9deg; --f2a: 179.3deg; }
.sb-page.sb-fa-1 { --f1a: 89.2deg; --f2a: 180.8deg; }
.sb-page.sb-fa-2 { --f1a: 91.4deg; --f2a: 178.9deg; }
.sb-page.sb-fa-3 { --f1a: 88.7deg; --f2a: 181.1deg; }
.sb-page.sb-fa-4 { --f1a: 90.4deg; --f2a: 178.4deg; }
.sb-page.sb-fa-5 { --f1a: 89.6deg; --f2a: 181.6deg; }
.sb-page.sb-fa-6 { --f1a: 91.8deg; --f2a: 179.8deg; }

.sb-page.sb-fp-0 { --f1p: 36%; --f2p: 47%; }
.sb-page.sb-fp-1 { --f1p: 62%; --f2p: 39%; }
.sb-page.sb-fp-2 { --f1p: 44%; --f2p: 58%; }
.sb-page.sb-fp-3 { --f1p: 57%; --f2p: 33%; }
.sb-page.sb-fp-4 { --f1p: 39%; --f2p: 63%; }

.sb-spread { position: absolute; inset: 0; z-index: 1; }
.sb-spread > .sb-page.left { left: 0; }
.sb-spread > .sb-page.right { left: 50%; }

/* The valley the two pages fold into. The reference gets this free — its plates
   are photographs of a real book — so this rule has no counterpart there.
 *
 * A DIRECT CHILD OF `.sb-book`, AND IT OUTLIVES EVERY BRANCH OF paint(). It is
 * the book's crease, not the resting spread's, so it is painted whether the
 * spread is lying flat or a leaf is in the air; see the note at its one append
 * site in sketchbook.js. `left: 50%` and the translate therefore resolve
 * against the book box, which `.sb-spread`'s `inset: 0` made the same box
 * anyway — the geometry did not move when the parent did.
 *
 * z-index 2 IS WHAT MAKES IT VISIBLE, and it is measured against `.sb-spread`
 * and `.sb-half` (both 1) — not against the leaf. Checked by setting it to 0:
 * the crease disappears completely, behind the resting spread and behind the
 * standing halves alike, because both of those are 1 and win.
 *
 * WHAT ORDERS IT AGAINST THE TURNING LEAF IS DEPTH, NOT THIS NUMBER, and the
 * `.sb-curl` z-index of 6 is a red herring for anyone reading these two rules
 * side by side. `.sb-book` is `transform-style: preserve-3d` and the curl
 * carries a `rotateY`, so the browser sorts the two by where they are in
 * space: a leaf standing up toward the reader covers the crease it is lifting
 * away from, and a leaf still lying flat does not. Checked by raising this to
 * 7 and holding a turn — nothing changes at either end of the swing. Which is
 * the behaviour we want, so nothing here needs defending; it just is not
 * defended by the z-index. */
.sb-gutter {
  position: absolute;
  top: 0; bottom: 0; left: 50%;
  width: 9%;
  transform: translateX(-50%);
  pointer-events: none;
  z-index: 2;
  background:
    linear-gradient(90deg,
      rgba(52, 38, 20, 0) 0%,
      rgba(52, 38, 20, .16) 44%,
      rgba(52, 38, 20, .26) 50%,
      rgba(52, 38, 20, .16) 56%,
      rgba(52, 38, 20, 0) 100%);
}

/* ---------- a slot on a page ----------
 *
 * The slot is the packer's box and nothing else: it never moves. The three
 * things inside it — the print, the caption and the note — each carry their own
 * saved transform, because each is dragged, resized and rotated independently
 * (see the record layout in state.js). */
.sb-slot {
  position: absolute;
  left:   calc(var(--sx) / 1000 * 100%);
  top:    calc(var(--sy) /  710 * 100%);
  width:  calc(var(--sw) / 1000 * 100%);
  height: calc(var(--sh) /  710 * 100%);
}

.sb-part {
  position: absolute;
  left: 0;
  right: 0;
  transform: translate(calc(var(--x, 0) * var(--pu)), calc(var(--y, 0) * var(--pu)))
             rotate(calc(var(--r, 0) * 1deg))
             scale(var(--s, 1));
  /* SCALING ABOUT THE MIDDLE IS WHAT MAKES THE RESIZE CHEAP. `scale` here is a
     transform rather than a width, so an item grows about its own centre and
     the gesture has nothing to correct — no measure, no reflow, none of the two
     writes attachResizeHandlers in polaroids.js has to make on every move. It
     is also what makes the rotate's rect safe to read once: rotation about the
     centre leaves the axis-aligned box centred on the same point at every
     angle. Both facts are relied on by name in sketchbook.js; changing the
     origin breaks two gestures that will look merely "off" rather than wrong. */
  transform-origin: 50% 50%;
  /* AND THIS IS THE HALF OF THE HIT-TEST THE STYLESHEET OWNS. The pages are
     `pointer-events: none` (see `.sb-zone` below), so a press falls through the
     paper to the turn zone underneath it; `auto` here is what stops it falling
     through a photo, a caption or a note. Delete this line and every drag on an
     item turns the page instead. */
  pointer-events: auto;
  cursor: grab;
  /* NO TRANSITION ON THE TRANSFORM, AND IT WAS TAKEN OUT RATHER THAN NEVER
     WRITTEN. `transition: transform .26s` was here first, so that a move made
     by the other person glided in rather than teleporting — the desk's 0.55s
     ease, scaled down. It went because it ALSO starts on the first paint of a
     saved item: a print rendered with --x: 126.47 already on it computed to
     `matrix(1,0,0,1,0,0)` — the transition's START value — and snapped to its
     real `matrix(1,0,0,1,86,-62)` the moment `transition: none` was set on it.
     So the transition is running from the authored slot on the very first
     frame, on every load and after every page turn.

     WHAT THAT LOOKS LIKE ON A VISIBLE PAGE IS INFERRED AND NOT MEASURED, and
     the distinction is the whole reason it was visible at all: the Chrome
     window it was found in was occluded, `document.hidden` was true, and an
     occluded page's animation timeline is frozen — which is why the value sat
     at the start instead of easing past it. On a page somebody is looking at it
     would ease, so what it costs is a quarter-second of every moved item
     sliding out of a slot it was never in, on every load. That is a flash of
     the wrong layout paid always, to smooth a remote move that happens rarely.

     Without it, an item is drawn where its record says it is, immediately, on
     every path — and a remote move snaps. That is the deliberate trade. The
     three classes below stayed, because what they carry is not the
     suppression. */
}

/* Held. The lift is so that a caption dragged over its own print goes over it
   rather than under, which is the only ordering anyone can predict while they
   are doing it; the cursor and the lit grips are the other two things these
   classes are for (see the grip rules below). */
.sb-part.sb-dragging,
.sb-part.sb-resizing,
.sb-part.sb-rotating {
  z-index: 1;
}
.sb-part.sb-dragging { cursor: grabbing; }

/* WHERE THE BOX ACTUALLY IS, which on the two text items is not where the words
   are. A caption's box is the full column the packer reserved — 560 page units
   wide and as many lines tall as it was given — so a one-line caption is a
   grabbable rectangle several times the size of its own text, and a note's box
   can reach out past the print above it onto what looks like bare paper. Shown
   on hover so that "what will I pick up if I press here" is answerable before
   pressing rather than after.
   `outline` and not a border or a pseudo-element: it takes no space, it does
   not disturb the -webkit-box the text is clamped in, and it follows the item's
   own transform. */
.sb-part:hover,
.sb-part.sb-dragging,
.sb-part.sb-resizing,
.sb-part.sb-rotating {
  outline: 1px dashed rgba(58, 43, 20, .30);
  outline-offset: 5px;
}

/* ---------- the two grips on an item ----------
 *
 * Inside the part, so they travel with it — which is also why they are counted
 * back out again for the plates of a turning leaf (see buildPlate).
 *
 * COUNTER-SCALED, AND THAT IS NOT A POLISH DETAIL. The part's transform scales
 * everything inside it, and the photo band runs down to 0.4: a 20px grip on an
 * item shrunk to its smallest would be 8px, too small to catch, and the only
 * way back up would be a gesture you can no longer start. `1 / --s` holds it at
 * 20px at every size. --s is inherited from `.sb-part`, and the fallback covers
 * an item nobody has ever touched.
 *
 * They straddle the corner — 20px wide at an offset of -10px puts the centre
 * exactly on it — so half of each hangs off the item and half sits on it. That
 * is deliberate: they are revealed by `.sb-part:hover`, and a grip that floated
 * clear of the item would drop the hover on the way to it and fade out from
 * under the pointer reaching for it. Hovering the overhang keeps the part
 * hovered, because the grip is a descendant of it.
 *
 * OPPOSITE CORNERS, AND THAT IS A FIX RATHER THAN A PREFERENCE. Both grips
 * started on the right-hand edge, one at each end of it, which is fine on a
 * print and unusable on the writing. Measured on a 1360px spread, a one-line
 * caption's box is 25.8px tall and its note sits just under it, so the
 * caption's bottom grip landed at (810, 644) and the note's top grip at
 * (810, 650): 6px apart at the same x, overlapping by 14 of their 20 pixels.
 * The note is later in the DOM, so the note won and the caption could not be
 * resized at all, with nothing on screen to say why. Diagonally opposite, the
 * nearest pair on that same spread is 25.8px apart — 5.8px of clear paper. */
.sb-grip {
  position: absolute;
  width: 20px;
  height: 20px;
  box-sizing: border-box;
  border-radius: 50%;
  border: 1px solid rgba(58, 43, 20, .5);
  background: rgba(255, 251, 242, .95);
  box-shadow: 0 1px 3px rgba(28, 20, 8, .35);
  opacity: 0;
  pointer-events: none;
  transition: opacity .16s ease;
  transform: scale(calc(1 / var(--s, 1)));
}
.sb-grip.sb-size { left: -10px; bottom: -10px; cursor: nesw-resize; }
/* Dark against the size grip's light, because two identical dots on one item
   is a coin toss every time you reach for one. */
.sb-grip.sb-turn {
  right: -10px; top: -10px;
  cursor: grab;
  background: rgba(58, 43, 20, .84);
  border-color: rgba(255, 251, 242, .72);
}

/* Revealed by hover, and KEPT LIT FOR THE DURATION OF A GESTURE. Both halves
   are load-bearing: a resize at either end of its band stops growing while the
   pointer does not, and a rotate swings the pointer off the item within the
   first few degrees — either way the pointer ends up somewhere `:hover` is
   false, and a grip that vanished from under the finger holding it reads as the
   gesture having died. Same rule, same reason, as `.polaroid.resizing
   .pl-handle` in style.css. */
.sb-part:hover > .sb-grip,
.sb-part.sb-dragging > .sb-grip,
.sb-part.sb-resizing > .sb-grip,
.sb-part.sb-rotating > .sb-grip {
  opacity: 1;
  pointer-events: auto;
}

/* A PRINTED PHOTO, NOT A POLAROID. The white margin is even on all four sides
   and there is no chin: the caption is set on the page beneath the print, which
   is the whole difference between this view and the desk. `padding` in percent
   resolves against the containing block's WIDTH on every side, so the border is
   square even though the box is not. */
.sb-print {
  /* Not 0: the print stands on the bottom of the area the packer reserved, so
     the gap to its caption is the same on every page. See renderSlot. */
  top: calc(var(--sprt) * var(--pu));
  /* Sized by printBox() in sketchbook.js, not by the slot: the box that holds
     the image at an EVEN border is not the box the packer reserved. It is never
     larger, so it stays inside the reserved area — read the long note there
     before changing either the 3.2% below or the BORDER beside it, because the
     two are one number written twice and nothing checks that they agree. */
  width: calc(var(--sprw) * var(--pu));
  height: calc(var(--sprh) * var(--pu));
  left: calc(50% - var(--sprw) * var(--pu) / 2);
  right: auto;
  box-sizing: border-box;
  background: #fff;
  /* --sb-border is BORDER in sketchbook.js, in page units, written onto the
     book once. It is read and not repeated because printBox() solves for the
     box that holds the image at exactly this border; a second copy here is a
     number the two can disagree about, and the disagreement is a picture that
     no longer fits its own frame. */
  padding: calc(var(--sb-border) * var(--pu));
  /* IN PAGE UNITS, NOT PIXELS. A print lies the same distance off the paper
     whichever card it is in, so this is relative to the BOOK and not to the
     card. 1.5 and 4.5 page units are the reference's 1px and 3px at the width
     this book opens to on a laptop; at a fixed 1px and 3px the shadow vanished
     under a full-page print and left it looking pasted on. */
  box-shadow: 0 calc(var(--pu) * 1.5) calc(var(--pu) * 4.5) rgba(0, 0, 0, .22);
}
/* EXIF ORIENTATION: LEAVE IT ALONE. THE DEFAULT IS WHAT AGREES WITH THE DESK.
 *
 * Three of the 55 files carry an EXIF orientation tag — sail-away
 * (images/Boat.jpeg, orientation 6), class-of-us (We in School.jpg, 8) and
 * first-date (First Date.JPG, 8) — read out of the files themselves, not
 * guessed. An <img> honours that tag by default (`image-orientation:
 * from-image`), so it is fair to worry that this view disagrees with the desk,
 * which paints every photo as a CSS background-image in polaroids.js.
 *
 * IT DOES NOT, BECAUSE THIS BROWSER HONOURS THE TAG FOR BACKGROUNDS TOO. Put
 * side by side in one page — the same file as `<img>` with the default, as
 * `<img>` with `image-orientation: none`, and as a background-image — the
 * default and the background are identical and upright, and only the `none`
 * one comes out lying on its side. The desk confirms it from the other
 * direction: all three of those drawings stand upright on it today.
 *
 * So `image-orientation: none` here would not fix a disagreement, it would
 * CREATE one, turning exactly those three files ninety degrees away from the
 * desk. It was added on that reasoning and taken out again on this
 * measurement. There is no rule to write; this comment is the whole of it.
 *
 * Nothing automated catches a regression here and this comment does not claim
 * otherwise — there is no DOM harness in this repo. The check is the side by
 * side above, and it takes a minute.
 *
 * WHAT DOES GET CHECKED is the other half of the same fact: an entry in
 * data/illustrations.json records the shape the browser PAINTS, which for
 * these three files is the rotated one. sail-away disagreed — 4032x3024 on
 * disk, orientation 6, so painted 3024x4032, and 0.75 in the data — and the
 * white frame printBox shaped from that number was landscape around a portrait
 * drawing. Corrected to 1.333, and tests/illustrations.test.js now reads the
 * dimensions and the orientation tag out of all 55 files and holds every entry
 * to what its own bytes say, so the next photo added with a rotation tag
 * cannot repeat it. */

.sb-print img {
  display: block;
  width: 100%;
  height: 100%;
  /* A BELT AND BRACES, NOT THE MECHANISM. printBox has already shaped the box
     to the aspect ratio the DATA claims, so there is normally nothing for this
     to do. It is what stops an image whose real proportions differ from its
     entry in illustrations.json from being stretched — and `contain` rather
     than `cover` because the cost of being wrong should be a little extra
     white, never a cropped drawing. */
  object-fit: contain;
  -webkit-user-drag: none;
  user-select: none;
}

/* THE CLAMP, AND WHY IT IS STILL NOT OPTIONAL.
 *
 * The design note says one photo per page removes the need for this rule. It
 * does not. It removes ONE of the three reasons text used to overflow — the
 * packed layout's scaling of a nominal measurement by k — and the other two
 * stand:
 *
 *   lines() in sketchbook-layout.js counts CHARACTERS at an average advance,
 *   and real text wraps on WORDS and honours a line break. Measured across all
 *   55 captions and notes on this site, rendered and counted against the lines
 *   the packer reserves: at a 912-unit column the spider-man note runs a line
 *   long, because it carries a blank line the estimate cannot see.
 *
 *   And the line caps TRUNCATE, on purpose. A note past NOTE_MAX_LINES is
 *   reserved seven lines and has more; something has to cut it, and the packer
 *   draws no pixels.
 *
 * At TEXT_W = 560 nothing on today's text overflows — that is what 560 was
 * chosen for — so this rule fires on nothing right now. It is not therefore
 * dead: it fires on the next note somebody writes past seven lines, which is
 * one message away, and on any column width that is retuned out of the clean
 * band. The height is exactly the box the packer reserved and `overflow`
 * contains it; the line-clamp only decides that the cut lands on an ellipsis.
 *
 * WIDTH, NOT left/right. The writing sets to the page's own text column, which
 * is not the slot's width — see the note on --stx in sketchbook.js.
 *
 * AND IT IS ON AN INNER ELEMENT RATHER THAN ON THE PART ITSELF, which is a
 * change this task had to make. The clamp needs `overflow: hidden`, and the
 * part is now also the thing that carries the two grips — half of each hanging
 * off the corner. On the part, the clamp's own overflow rule cut them in half.
 * `.sb-ink` is the same box (`inset: 0` inside a part that is already
 * positioned), so the text renders identically; what changed is that the box
 * doing the clipping is no longer the box the grips live in. */
.sb-cap, .sb-note {
  left: calc(var(--stx) * var(--pu));
  width: calc(var(--stw) * var(--pu));
  right: auto;
}
.sb-cap > .sb-ink, .sb-note > .sb-ink {
  position: absolute;
  inset: 0;
  overflow: hidden;
  display: -webkit-box;
  -webkit-box-orient: vertical;
  white-space: pre-wrap;
  overflow-wrap: break-word;
}
/* A LINE IS AS TALL AS THE PACKER SAID IT WOULD BE. --caplh and --notelh are
   CAP_LINE_H and NOTE_LINE_H, written onto the book from sketchbook-layout.js
   rather than copied, so the box that file reserves and the type drawn into it
   are one number. They are PAGE units, not card units: a page holds one photo,
   the text sets to the full measure beneath it, and there is no k left to scale
   by. The font sizes are fractions of the line, and that ratio IS the
   stylesheet's own — it belongs to the face, not to the layout. */
.sb-cap {
  top: calc(var(--scy) * var(--pu));
  height: calc(var(--sch) * var(--pu));
  font-family: 'Caveat', cursive;
  font-weight: 600;
  font-size: calc(var(--caplh) * var(--pu) * .90);
  line-height: calc(var(--caplh) * var(--pu));
  color: var(--sb-ink);
}
.sb-note {
  top: calc(var(--sny) * var(--pu));
  height: calc(var(--snh) * var(--pu));
  font-family: 'Kalam', cursive;
  font-size: calc(var(--notelh) * var(--pu) * .74);
  line-height: calc(var(--notelh) * var(--pu));
  color: var(--sb-ink-soft);
}
/* CAP_MAX_LINES and NOTE_MAX_LINES. The heights above are what actually contain
   the text; these only decide that the cut lands on an ellipsis. */
.sb-cap > .sb-ink { -webkit-line-clamp: 3; }
.sb-note > .sb-ink { -webkit-line-clamp: 7; }

/* ---------- the two halves that stand still during a turn ---------- */
.sb-half {
  position: absolute;
  top: 0; bottom: 0;
  width: 50%;
  overflow-x: clip;
  overflow-y: visible;
  z-index: 1;
}
.sb-half.left { left: 0; }
.sb-half.right { left: 50%; }
/* Said rather than left to the static position, which would happen to be the
   same: a page inside a face gets its `left` inline, and one rule silently
   depending on the absence of another is how those two get confused. */
.sb-half > .sb-page { left: 0; }

/* the shadow the lifting leaf throws onto the page beneath it */
.sb-gutter-shade {
  position: absolute;
  top: var(--pg, 21.8%); bottom: var(--pg, 21.8%);
  width: 46%;
  pointer-events: none;
  z-index: 3;
  opacity: calc(var(--shade, 0) * .62);
  -webkit-mask-image: linear-gradient(180deg, transparent 0, #000 5.2%, #000 94.8%, transparent 100%);
  mask-image: linear-gradient(180deg, transparent 0, #000 5.2%, #000 94.8%, transparent 100%);
}
.sb-gutter-shade.left { right: 0; background: linear-gradient(270deg, rgba(52, 38, 20, .30), rgba(52, 38, 20, 0) 82%); }
.sb-gutter-shade.right { left: 0; background: linear-gradient(90deg, rgba(52, 38, 20, .24), rgba(52, 38, 20, 0) 82%); }

/* ---- the turning leaf: nested strips forming a curved surface ---- */
.sb-curl {
  position: absolute;
  top: 0;
  height: 100%;
  width: calc(var(--bw, 0px) * var(--span));
  transform-style: preserve-3d;
  z-index: 6;
}
.sb-curl.next { left: 50%; transform-origin: left center; transform: rotateY(calc(-1 * var(--tt, 0deg))); }
.sb-curl.prev { right: 50%; transform-origin: right center; transform: rotateY(var(--tt, 0deg)); }
.sb-strip {
  position: absolute;
  top: 0;
  height: 100%;
  width: calc(var(--bw, 0px) * var(--span) / var(--n));
  transform-style: preserve-3d;
}
.sb-curl.next .sb-strip { transform-origin: left center; }
.sb-curl.prev .sb-strip { transform-origin: right center; }
.sb-curl.next > .sb-strip { left: 0; }
.sb-curl.prev > .sb-strip { right: 0; left: auto; }
.sb-curl.next .sb-strip .sb-strip { left: 100%; transform: rotateY(var(--td, 0deg)); }
.sb-curl.prev .sb-strip .sb-strip { right: 100%; transform: rotateY(calc(-1 * var(--td, 0deg))); }

/* `right: -1.1px` is the reference's, and it is a seam bleed: an exact edge
   leaves a hairline of background between adjacent strips at some widths.
   `overflow: hidden` is ours — the reference clips by painting a background,
   which never spills; we clip a real element. */
.sb-face {
  position: absolute;
  top: 0; bottom: 0; left: 0; right: -1.1px;
  overflow: hidden;
  backface-visibility: hidden;
  -webkit-backface-visibility: hidden;
}
.sb-face.back { transform: rotateY(180deg); }
.sb-face .sb-sh, .sb-face .sb-gl {
  -webkit-mask-image: linear-gradient(180deg, transparent 0, #000 5.2%, #000 94.8%, transparent 100%);
  mask-image: linear-gradient(180deg, transparent 0, #000 5.2%, #000 94.8%, transparent 100%);
}
.sb-strip.edge .sb-face .sb-sh, .sb-strip.edge .sb-face .sb-gl {
  -webkit-mask-image: linear-gradient(180deg, transparent 0, #000 9%, #000 91%, transparent 100%), var(--hf);
  mask-image: linear-gradient(180deg, transparent 0, #000 9%, #000 91%, transparent 100%), var(--hf);
  -webkit-mask-composite: source-in;
  mask-composite: intersect;
}
.sb-curl.next .sb-strip.edge .sb-face.front, .sb-curl.prev .sb-strip.edge .sb-face.back {
  --hf: linear-gradient(90deg, #000 0 22%, transparent 96%);
}
.sb-curl.next .sb-strip.edge .sb-face.back, .sb-curl.prev .sb-strip.edge .sb-face.front {
  --hf: linear-gradient(270deg, #000 0 22%, transparent 96%);
}
.sb-face .sb-sh {
  position: absolute; left: 0; right: 0;
  top: var(--pg, 21.8%); bottom: var(--pg, 21.8%);
  pointer-events: none;
  z-index: 2;
}
.sb-curl.next .sb-face.front .sb-sh, .sb-curl.prev .sb-face.back .sb-sh {
  background: linear-gradient(90deg, rgba(58, 43, 20, var(--a1, 0)), rgba(58, 43, 20, var(--a2, 0)));
}
.sb-curl.next .sb-face.back .sb-sh, .sb-curl.prev .sb-face.front .sb-sh {
  background: linear-gradient(90deg, rgba(58, 43, 20, var(--a2, 0)), rgba(58, 43, 20, var(--a1, 0)));
}
.sb-face .sb-gl {
  position: absolute; left: 0; right: 0;
  top: var(--pg, 21.8%); bottom: var(--pg, 21.8%);
  pointer-events: none;
  z-index: 2;
  background: #fffaf0;
  opacity: calc(var(--shade, 0) * var(--lit, 1) * var(--lit, 1) * .20);
}

/* ---------- the press targets ----------
 *
 * These cover the whole book, and until items became draggable they sat ABOVE
 * it at z-index 60 — every press anywhere on the paper started a turn, which is
 * the reference's behaviour and was right while nothing on a page could be
 * picked up. It is wrong now: a press on a print, a caption or a note has to
 * reach that element.
 *
 * SO THE ZONES WENT UNDER THE PAPER AND THE PAPER BECAME A HOLE. Two rules do
 * it together and neither works alone:
 *
 *   z-index 0 here, against 1 on `.sb-spread` and `.sb-half`. The paper — and
 *   therefore everything on it — is in front, so a press on an item reaches the
 *   item. (Ordering inside `.sb-book` is a mixture of stacking and depth,
 *   because the book is `preserve-3d`; see the long note on `.sb-gutter`. It is
 *   stacking that decides this one: the zones and the resting spread are both
 *   flat at the same depth, so z-index is all there is between them.)
 *
 *   `pointer-events: none` on the pages, restored by `auto` on `.sb-part`. A
 *   press on bare paper has to fall THROUGH to the zone it is now in front of,
 *   or the book stops turning altogether. ALL FOUR SELECTORS EARN THEIR PLACE,
 *   measured one at a time with `document.elementFromPoint` over bare paper:
 *   drop `.sb-spread` and a press at rest lands on `.sb-spread` itself; drop
 *   `.sb-half` and `.sb-curl` and, with a leaf held in the air, a press over
 *   the leaf lands on `.sb-face` and one over the standing half lands on
 *   `.sb-half`. Each of those is a book that has stopped turning.
 *
 * Both halves were checked by hand rather than by a test — a press landing on
 * one element rather than another is exactly what no DOM stub in this repo
 * models. `elementFromPoint` IS the browser's own hit test, so it answers the
 * question a press asks: at the middle of a print it names the `<img>` with the
 * zones at 0 and `.sb-zone.sb-prev` with them back at 60. Drag a photo, then
 * drag the paper beside it. */
.sb-spread,
.sb-half,
.sb-page,
.sb-curl {
  pointer-events: none;
}
.sb-zone {
  position: absolute;
  top: 0; bottom: 0;
  border: 0;
  background: transparent;
  cursor: grab;
  z-index: 0;
  -webkit-tap-highlight-color: transparent;
}
.sb-zone:active { cursor: grabbing; }
.sb-zone.sb-prev { left: 0; width: 50%; }
.sb-zone.sb-next { right: 0; width: 50%; }

/* ---------- what is written under the book ---------- */
.sb-foot {
  display: flex;
  align-items: baseline;
  gap: 14px;
  color: rgba(255, 255, 255, .62);
  font-family: 'Caveat', cursive;
  font-size: 21px;
}
.sb-foot .sb-count {
  font-family: 'Kalam', cursive;
  font-size: 12px;
  letter-spacing: .14em;
  color: rgba(255, 255, 255, .4);
}
.sb-close {
  border: 1px solid rgba(255, 255, 255, .28);
  background: transparent;
  color: rgba(255, 255, 255, .72);
  font-family: 'Kalam', cursive;
  font-size: 12px;
  letter-spacing: .12em;
  padding: 5px 14px;
  border-radius: 999px;
  cursor: pointer;
  justify-self: start;
  margin-left: 12px;
}
.sb-close:hover { color: #fff; border-color: rgba(255, 255, 255, .6); }

/* ---------- the glass, and the copy of the book it magnifies ----------

   PORTED FROM THE REFERENCE (meng-to-sketchbook.html, lines 243-347): the
   bezel's brass ramp, the handle, the two gradients that make the lens read as
   a dome, and the wrapper the magnified copy is masked inside. The one node not
   ported is `.lens .mag`, which is `display: none` there and written to by
   nothing — see the note in sketchbook.js's open().

   The glass really is see-through: the bezel is an ANNULUS, punched out by its
   own mask, so what is behind it is the magnified copy and not a painted disc. */
.loupe {
  position: absolute;
  left: 0; top: 0;
  width: var(--lr, 240px);
  height: var(--lr, 240px);
  pointer-events: none;
  /* Over the paper (1 and 2) and over the press targets (0), and under nothing
     inside the book — it is an object lying on the page. */
  z-index: 80;
  opacity: 0;
  transition: opacity .25s ease;
  will-change: transform;
}
.loupe.on { opacity: 1; }
.loupe.held .ring { cursor: grabbing; }
/* `pointer-events` comes back on for the two parts you can actually take hold
   of, so the rest of the square the glass occupies stays a hole. */
.loupe .ring {
  position: absolute;
  inset: 0;
  border-radius: 50%;
  pointer-events: auto;
  cursor: grab;
  padding: calc(var(--lr, 240px) * .058);
  box-shadow:
    0 1px 2px rgba(58, 44, 26, .30),
    0 10px 18px rgba(58, 44, 26, .24),
    0 26px 40px rgba(58, 44, 26, .20),
    0 48px 66px rgba(58, 44, 26, .13);
}
.loupe .ring::before {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: 50%;
  pointer-events: none;
  background: linear-gradient(146deg,
    #fdf7e9 0%, #e6d7b4 14%, #b69d70 32%, #7d6740 50%,
    #cdbb92 66%, #f4ead3 80%, #9b8459 100%);
  box-shadow:
    inset 0 1px 1px rgba(255, 255, 255, .8),
    inset 0 -2px 3px rgba(70, 52, 26, .5);
  -webkit-mask-image: radial-gradient(circle closest-side at 50% 50%, transparent 0 88.2%, #000 89.8% 100%);
  mask-image: radial-gradient(circle closest-side at 50% 50%, transparent 0 88.2%, #000 89.8% 100%);
}
/* The handle, drawn before the bezel in the markup so the ring covers its
   inner end. */
.loupe .grip {
  position: absolute;
  left: 50%; top: 50%;
  width: calc(var(--lr, 240px) * .74);
  height: calc(var(--lr, 240px) * .125);
  transform-origin: 0 50%;
  transform: rotate(40deg) translate(calc(var(--lr, 240px) * .33), -50%);
  border-radius: calc(var(--lr, 240px) * .06);
  pointer-events: auto;
  cursor: grab;
  background:
    linear-gradient(180deg, rgba(255, 255, 255, .46) 0 13%, rgba(255, 255, 255, 0) 44%,
                    rgba(0, 0, 0, .26) 100%),
    linear-gradient(90deg, #d9bd82 0 14%, #a9884e 14% 20%, #6d4c2b 20% 62%,
                    #5a3d22 62% 92%, #7a563180 92% 100%);
  box-shadow: 0 8px 15px rgba(58, 44, 26, .26), 0 18px 26px rgba(58, 44, 26, .14);
}
.lens {
  position: relative;
  display: block;
  width: 100%;
  height: 100%;
  border-radius: 50%;
  overflow: hidden;
  box-shadow:
    inset 0 0 0 1px rgba(52, 40, 22, .55),
    inset 0 4px 12px rgba(40, 30, 14, .28),
    inset 0 -7px 16px rgba(255, 250, 240, .14);
}
.lens::before, .lens::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: 50%;
  pointer-events: none;
}
.lens::before { z-index: 1; }
.lens::after { z-index: 2; }
/* The glass bends and darkens toward its rim, with a faint colour fringe. */
.lens::before {
  background: radial-gradient(circle at 50% 50%,
    rgba(0, 0, 0, 0) 54%, rgba(58, 44, 26, .10) 76%, rgba(46, 34, 16, .34) 100%);
  box-shadow:
    inset 0 0 0 2px rgba(130, 162, 196, .26),
    inset 0 0 0 4px rgba(206, 158, 112, .15);
}
/* The dome: one broad specular and a tight crescent opposite it. */
.lens::after {
  background:
    radial-gradient(36% 26% at 29% 19%, rgba(255, 255, 255, .30), rgba(255, 255, 255, 0) 76%),
    radial-gradient(24% 16% at 74% 86%, rgba(255, 255, 255, .12), rgba(255, 255, 255, 0) 80%),
    linear-gradient(150deg, rgba(255, 255, 255, .06) 0 18%, rgba(255, 255, 255, 0) 42%);
}

/* A second copy of the book, sitting beside the glass OUTSIDE the tilt and
   scaled about whichever page point the glass is currently over. `inset: 0` on
   `.sb-3d`, whose box is the book's, so the copy lands exactly over the
   original before placeLoupe() moves it. The circle you see through the lens is
   a mask written onto this wrapper, not a clip on the lens: the lens is a
   sibling in front of it, and its rim and highlights paint over the top. */
.zoomwrap {
  position: absolute;
  inset: 0;
  overflow: hidden;
  pointer-events: none;
  z-index: 2;
  opacity: 0;
}
.zoominner { position: absolute; inset: 0; transform-origin: 0 0; }

/* THE COPY IS A PICTURE OF THE BOOK AND NOT A SECOND BOOK, and this rule is
   the whole of what makes that true. `pointer-events: none` on `.zoomwrap`
   above does NOT hold on its own: `.sb-part` sets it back to `auto`, exactly
   as it does through the pages' own `none` — that is the ladder documented
   over `.sb-zone` — so every print, caption and note in the copy became a hit
   target of its own, sitting at `.zoomwrap`'s z-index 2 in front of the real
   ones. It is not a near miss either: the copy is scaled about the page point
   under the glass and clipped to the whole book, so its parts land all over
   the spread, nowhere near the lens.

   Measured with `document.elementFromPoint` at the middle of a real caption
   while the glass was on: it named an `.sb-ink` inside `.zoominner`, and a
   real CDP-driven drag on that caption moved nothing at all — the whole of
   Task 8's gestures, dead, wherever the copy happened to fall.

   TWO CLASS SELECTORS DELIBERATELY. `.zoomwrap *` alone ties with `.sb-part`
   on specificity and would be decided by which rule is written last, which is
   not a thing to leave to the order of a stylesheet. */
.sb-3d .zoomwrap * { pointer-events: none; }

/* ---------- the book's own controls ----------
   THERE IS NO BAR UNDER THE BOOK ANY MORE. The reference puts its controls
   there and so did this file; the book scales about its own centre, so a few
   presses of "zoom in" walked the paper down over the bar and left the zoom
   out of reach behind the page it had just enlarged. The two controls worth
   keeping — the magnifier and the spread reset — moved to the fixed corner
   group, which nothing on this layer can cover, and the zoom went with the bar.

   THEIR LOOK IS style.css's, not this file's: `.sb-tools` and `.sb-tool` are
   named in the same rules as `.bg-switcher`/`.view-switcher` and their
   buttons, so the three pills in that corner cannot drift apart. What is left
   here is the one thing that is about the BOOK rather than about the corner. */

/* A FINGER CANNOT PUT A GLASS DOWN AND LEAVE IT SOMEWHERE, so the loupe goes
   on a coarse pointer. The reference hides `.loupe` alone; `.zoomwrap` is a
   SEPARATE ELEMENT beside it, so that would have left a phone with a magnified
   disc of page floating on the paper and no magnifier on it. Both go, and so
   does the button that would turn a glass nobody can see back on. sketchbook.js
   reads the same media query and starts with the glass off, so the copy is not
   built either — the rules here are what it would LOOK like, not what it
   costs. */
@media (pointer: coarse) {
  .loupe, .zoomwrap, .sb-glass, .sb-tool-sep { display: none; }
}

/* Nothing on the book is selectable text or a draggable image: both are
   gestures that fight the page turn. */
.sb-wrap, .sb-wrap * { -webkit-user-select: none; -moz-user-select: none; user-select: none; }

@media (max-width: 640px) {
  .sb-arrow { position: absolute; top: 50%; transform: translateY(-50%); z-index: 70; padding: 12px 6px; }
  .sb-arrow.left { left: 2px; }
  .sb-arrow.right { right: 2px; }
  /* On `.sb-wrap` and not on `.sb-3d`, because that is where the declaration
     is: this is one of the frame's lengths and they are all declared together
     up there. `.sb-3d` reads it and the rail of ribbons narrows with the book
     without naming a width at all, being sized by its containing block. */
  .sb-wrap { --sb-book-w: min(98vw, calc(74vh * 2000 / 710)); }
}

/* A ribbon grows when the cursor lands on it and when it becomes one of the
   open pair, and both of those are motion somebody may have asked not to have.
   sketchbook.js answers the same preference for the page turn itself (see
   REDUCED), the same way: the thing still happens, it just happens at once.
   The rail's own scrolling needs nothing here — it is already instant, for the
   reason written over `.sb-rail`. */
@media (prefers-reduced-motion: reduce) {
  .sb-mark { transition: none; }
}
