/* ═══════════════════════════════════════════════════════════════
   A room at about four in the afternoon.

   Palette thesis: neutrals ROTATE HUE as they darken. What the sun
   lands on drifts yellow-green; what only sky reaches drifts
   periwinkle. Shadows are never black — a sensor never records zero
   chroma.

   Typography: ONE mono usage (numerals, where tabular figures earn
   it). Secondary text is italic serif, the way a book sets it —
   not uppercase mono with wide tracking on every label.

   No images. Grain and wood fibre are SVG turbulence, inline.
   ═══════════════════════════════════════════════════════════════ */
:root{
  --lx:.66;  --ly:-.74;  --throw:20px;
  --sx:calc(var(--lx) * -1 * var(--throw));
  --sy:calc(var(--ly) * -1 * var(--throw));

  --shade:74 84 118;
  --warm:255 233 184;
  --bounce:226 175 98;

  --wall-sun:#FBF7E9;  --wall-lit:#EFECDD;  --wall-mid:#E2E1D6;
  --wall-shade:#CDD1D6; --wall-deep:#B5BAC5;

  --frame:#F7F3E7;  --bar:#EDE7D4;
  --sky-top:#9CC0DC; --sky-mid:#B8CFDF; --horizon:#E9E3CE;
  --leaf-lit:#C9CF9E; --leaf-mid:#9CAF7C; --leaf-dark:#6F8467;

  --oak-edge:#F1E3C3; --oak-sun:#E4D2AF; --oak-face:#D2C2A2;
  --oak-shade:#A79C84; --bay-1:#A79C82; --bay-2:#7E7561;

  --paper:#FAF9F5;  --paper-warm:#FFFCF1;
  --ink:#22262F;    --ink-2:#4E5361;  --ink-3:#6B7080;
  --rule:#DFDDD3;   --cypress:#2A5D53; --tint:#D9E4DD;

  --serif:"Iowan Old Style",Charter,"Bitstream Charter","Sitka Text",Cambria,Georgia,serif;
  --mono:ui-monospace,"SF Mono",SFMono-Regular,Menlo,Consolas,monospace;

  /* Two octaves, not four and five. Turbulence is re-evaluated every time
     these tiles are rasterised, and a pinch-zoom rasterises on every step —
     they were the single most expensive thing the renderer did. The octaves
     past the second land under a .05–.11 opacity overlay, i.e. below the
     threshold where anyone can see them: compared at 3× on the covers, the
     wood and the spines, the two are indistinguishable. Measured over five
     interleaved zoom trials this took raster from 2156ms to 1904ms. */
  --grain:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='170' height='170'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.92' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='170' height='170' filter='url(%23n)'/%3E%3C/svg%3E");
  --fibre:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='460' height='120'%3E%3Cfilter id='w'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.012 0.62' numOctaves='2' seed='11'/%3E%3C/filter%3E%3Crect width='460' height='120' filter='url(%23w)'/%3E%3C/svg%3E");

  /* Every book is measured in --u. Bounded by BOTH axes, so the whole
     bookcase lands inside one screen with nothing to scroll:
       width  — the wider shelf is the TOP one at 1101u + 16px of padding
                (1048u below it). Measured by summing the flex items, and
                note the top row has 8 of them — trophy, five boards, the
                medal's hanger, ball — so seven gaps, not four. Wider
                boards flipped which shelf governs this, and the case spilled
                past the uprights at ~700-900px until the divisor followed.
       height — case + cat is ~654 units, plus ~324px of fixed chrome
     Height fell from 748 when the two bays were equalised at 244u each,
     which is why the whole case now renders bigger on the same screen.
     The ceiling is deliberate, twice over: a ceiling at all so the case
     parks (at ~914px) and does NOT keep growing on large displays — the
     composition stays put — and .75px rather than 1px because the
     full-size room read as oversized on a desktop (chosen by eye against
     the 1px original, July 2026). The px-sized furnishings — masthead,
     window, bubble, shelf note, spread — step down by the same .75 in
     the min-width:961px block at the bottom. Below the ceiling nothing
     changed: the same derivation still fits every width, so phones and
     tablets render exactly as they always did. */
  --ub:var(--u);          /* books on the library shelf; see the phone rules */
  --u:min(.75px, calc((100vw - 88px) / 1101), calc((100vh - 324px) / 654));
}

*{box-sizing:border-box;}

html{
  min-height:100%;
  background:linear-gradient(255deg, var(--wall-sun) 0%, var(--wall-lit) 26%, var(--wall-mid) 52%, var(--wall-shade) 78%, var(--wall-deep) 100%) fixed;
}
/* The warm glow the window throws on the wall. It used to be the first
   layer of the html background, a radial evaluated across the WHOLE
   viewport even though it is transparent past its 56% stop — at every
   zoom step, which made it the most expensive single paint on the page
   (~20% of raster). Same gradient, same geometry, evaluated only over
   the corner it actually lights: the box is 66vw x 40vh, so the radii
   and centre are scaled up to land in the same place on screen
   (88vw/66vw = 133%, 60vh/40vh = 150%, centre 86vw -> 78.8%). */
html::before{
  content:"";position:fixed;top:0;right:0;
  width:66vw;height:40vh;
  z-index:-1;pointer-events:none;
  background:radial-gradient(133% 150% at 78.8% 10%, rgb(var(--warm) / .5) 0%, transparent 56%);
}
body{
  margin:0;
  min-height:100vh;
  display:flex;flex-direction:column;justify-content:center;
  padding:22px 20px 26px;
  background:transparent;
  color:var(--ink);
  font-family:var(--serif);
  overflow-x:hidden;
}
body::before{
  content:"";position:fixed;inset:0;pointer-events:none;z-index:0;
  background-image:var(--grain);opacity:.05;mix-blend-mode:multiply;
}

a{color:inherit;}
button{font:inherit;color:inherit;background:none;border:0;cursor:pointer;padding:0;}
:focus-visible{outline:2px solid var(--cypress);outline-offset:4px;border-radius:2px;}

/* the room column matches the parked case EXACTLY (measured: the case's
   own max-content width at --u = .75 is 871.8px) so the masthead and the
   window sit flush with the uprights — derive it again if the cap moves */
.room{position:relative;z-index:1;width:100%;max-width:872px;margin:0 auto;}

/* the patch of sun the window throws on the plaster */
.sunpatch{
  position:absolute;top:6%;right:20%;
  width:min(400px,40vw);height:min(360px,38vw);
  pointer-events:none;z-index:0;
  background:
    linear-gradient(92deg, transparent 47.2%, rgb(var(--shade) / .1) 47.6% 49.4%, transparent 49.8%),
    linear-gradient(3deg,  transparent 45.5%, rgb(var(--shade) / .1) 46% 47.8%, transparent 48.2%),
    radial-gradient(72% 66% at 42% 36%, rgb(var(--warm) / .72) 0%, rgb(var(--warm) / .38) 46%, transparent 74%);
  transform:rotate(-13deg) skewX(-17deg);
  filter:blur(7px);mix-blend-mode:screen;
}

/* ═══ MASTHEAD ════════════════════════════════════════════════ */
.top{
  position:relative;z-index:2;
  margin-bottom:clamp(12px,1.8vh,22px);
}
.masthead{max-width:min(46ch, calc(100% - 230px));}

h1{
  margin:0;
  font-size:clamp(29px,3.5vw,41px);
  font-weight:400;
  letter-spacing:-.021em;      /* a large serif needs negative tracking */
  line-height:1.02;
}
.sub{
  margin:11px 0 0;max-width:46ch;
  font-size:clamp(15px,1.25vw,16.5px);
  line-height:1.58;color:var(--ink-2);
}
/* the epigraph: italic line, then the speaker set small in mono on
   its own line, the way a book credits one */
.sub.epi{font-style:italic;}
.sub .who{
  display:block;margin-top:3px;font-style:normal;
  font-family:var(--mono);font-size:11px;letter-spacing:.09em;
  text-transform:uppercase;color:var(--ink-3);
}
/* the dictionary entry: headword with syllable dots, a quiet mono
   respelling, italic part of speech, then the definition in roman */
.sub.defn .word{font-weight:400;font-size:18px;letter-spacing:.01em;color:var(--ink);}
.sub.defn .pron{margin:0 2px;font-family:var(--mono);font-size:12px;color:var(--ink-3);}
.sub.defn .pos{margin-right:2px;color:var(--ink-3);}
/* secondary information is italic, the way a book sets it — not
   uppercase mono with wide tracking, which is the templated tell */
.meta{
  margin:12px 0 0;
  font-size:14px;font-style:italic;
  color:var(--ink-3);
}
.meta a{
  font-style:normal;color:var(--cypress);
  text-decoration:underline;
  text-decoration-color:rgb(42 93 83 / .34);
  text-decoration-thickness:1px;
  text-underline-offset:3px;
  transition:text-decoration-color .18s;
}
.meta a:hover{text-decoration-color:var(--cypress);}
.meta .sep{opacity:.5;margin:0 7px;}

/* ── window ─────────────────────────────────────────────────── */
.window{position:absolute;top:8px;right:0;z-index:2;width:clamp(132px,14vw,186px);}
.win-glass{
  position:relative;aspect-ratio:1 / 1.16;
  background:linear-gradient(179deg,
    var(--sky-top) 0%, var(--sky-mid) 34%, var(--horizon) 52%,
    var(--leaf-lit) 58%, var(--leaf-mid) 78%, var(--leaf-dark) 100%);
  box-shadow:inset 0 0 24px rgb(var(--shade) / .22);
  overflow:hidden;
}
.win-glass::before{
  content:"";position:absolute;left:6%;bottom:-14%;width:64%;height:52%;
  border-radius:52% 48% 44% 56%;
  background:radial-gradient(closest-side, var(--leaf-dark), rgb(111 132 103 / 0));
  opacity:.6;filter:blur(6px);
}
.win-glass::after{
  content:"";position:absolute;inset:0;
  background:radial-gradient(56% 42% at 62% 22%, rgb(255 252 240 / .8) 0%, transparent 68%);
}
.win-bars{position:absolute;inset:0;z-index:2;}
.win-bars i{position:absolute;background:var(--bar);box-shadow:0 1px 0 rgb(var(--shade) / .28);}
.win-bars i.v{top:0;bottom:0;left:calc(50% - 2.5px);width:5px;}
.win-bars i.h{left:0;right:0;top:calc(46% - 2.5px);height:5px;}
.win-frame{
  position:absolute;inset:-11px -11px -9px;
  border:11px solid var(--frame);border-bottom-width:9px;
  box-shadow:0 1px 0 rgb(255 255 255 / .8) inset,
             var(--sx) var(--sy) calc(var(--throw) * 1.5) calc(var(--throw) * -.5) rgb(var(--shade) / .28);
  pointer-events:none;
}
.win-sill{
  height:9px;margin:11px -18px 0;
  background:linear-gradient(180deg, var(--oak-edge) 0 1px, var(--oak-sun) 1px 55%, var(--oak-face) 100%);
  border-radius:1px;
  box-shadow:0 1px 0 rgb(var(--shade) / .48), 0 3px 6px -2px rgb(var(--shade) / .32);
}

/* ═══ BOOKCASE ════════════════════════════════════════════════ */
.case{position:relative;z-index:2;width:max-content;max-width:100%;margin:0 auto;}

.perch{
  --catw:calc(250 * var(--u));
  position:relative;z-index:7;
  height:calc(var(--catw) * .509 - 6px);   /* floor of the artwork, less 6px so it rests on the wood */
  display:flex;align-items:flex-start;justify-content:flex-end;
  padding-right:calc(46 * var(--u));
  pointer-events:none;
}
.perch svg{
  width:var(--catw);height:calc(var(--catw) * .591);
  overflow:visible;display:block;flex:0 0 auto;
}
/* The cat's thought bubble — a THOUGHT bubble, not speech: the cat is
   asleep, so guidance arrives as dreams. The old hints still rotate
   through, but every other thought is now the standing ask —
   "recommend him a book? click me" — which wears cypress (.ask) so it
   reads as the link it is. The whole bubble is a button that opens
   the recommendation slip whenever it's clicked. Floats up-left of
   the head (which faces left), dots trailing toward it; positioned
   in --u so it keeps its seat beside the cat at every size.
   pointer-events is INHERITED and .perch turns it off — the bubble
   has to switch itself back on or it's decoration, not a button. */
.dream{
  position:absolute;z-index:7;pointer-events:auto;cursor:pointer;
  right:calc(46 * var(--u) + var(--catw) * .96);
  bottom:calc(var(--catw) * .38);
  padding:9px 16px 10px;
  background:var(--paper-warm);
  border:1px solid var(--rule);
  border-radius:18px 18px 18px 6px;
  box-shadow:0 3px 12px -4px rgb(var(--shade) / .38);
  font-family:var(--serif);   /* a <button> stops inheriting the room's type */
  font-size:15.5px;font-style:italic;color:var(--ink);
  letter-spacing:.004em;
  white-space:nowrap;
  transform-origin:92% 130%;   /* grows out of the trail of dots */
  /* the thought ARRIVES: dot, bigger dot, then the bubble pops — each
     `backwards` so nothing shows before its turn — and once it is up it
     breathes on the cat's rhythm. The float waits for the pop to finish
     so the two transforms never fight over the same frames. */
  animation:dreamIn .5s cubic-bezier(.34,1.56,.64,1) 1.5s backwards,
            dreamFloat 4.8s ease-in-out 2.4s infinite;
}
.dream::before,.dream::after{
  content:"";position:absolute;border-radius:50%;
  background:var(--paper-warm);border:1px solid var(--rule);
}
.dream::before{width:10px;height:10px;right:-11px;bottom:-9px;
  animation:dotIn .32s cubic-bezier(.34,1.56,.64,1) 1.05s backwards;}
.dream::after{width:5px;height:5px;right:-19px;bottom:-17px;
  animation:dotIn .32s cubic-bezier(.34,1.56,.64,1) .65s backwards;}
.dream:hover{border-color:var(--cypress);color:var(--cypress);}
/* the ask slots read as the link they are; hints keep the ink colour.
   The fade lives on the span so the bubble and its dots hold still
   while the words change. */
.dream.ask i{color:var(--cypress);}
.dream i{font-style:inherit;transition:opacity .24s ease;}
@keyframes dreamIn{from{opacity:0;transform:scale(.25)}}
@keyframes dotIn{from{opacity:0;transform:scale(.2)}}
@keyframes dreamFloat{0%,100%{transform:translateY(0)}42%{transform:translateY(-2.5px)}80%{transform:translateY(0)}}

/* The objects live ON THE SHELVES, beside the books — a shelf is where
   you actually keep this stuff. They ship inside .props and the script
   relocates them into the two rows, so the big SVGs stay in one block
   of markup. */
.props{display:none;}
.prop{
  display:block;flex:0 0 auto;position:relative;
  transition:transform .3s cubic-bezier(.22,.9,.3,1);
}
.prop:hover,.prop:focus-visible{transform:translateY(calc(-6 * var(--u)));}
.prop svg{display:block;overflow:visible;}
/* when the WebGL layer is live, the SVGs yield but the buttons stay
   as the hit areas and focus targets */
.prop.gl-on svg{visibility:hidden;}
.prop.gl-on:hover,.prop.gl-on:focus-visible{transform:none;}
/* Sized for LEGIBILITY, not physical proportion. A real tennis ball is
   a fifth the height of a trophy; at this scale that reads as a speck,
   so the small objects are pushed up until each one carries its own
   visual weight. Five objects of similar mass, spread out, beats one
   big thing plus four specks. */
/* Sized against the BOOKS they now stand next to, not against each
   other: the trophy reads at ~2/3 a display board, the ball a little
   under half. On the lower shelf true proportion finally works, because
   a tennis ball really is about a quarter of a spine's height. */
.prop.trophy svg{width:calc(131 * var(--u));height:calc(180 * var(--u));}
.prop.ball svg{width:calc(150 * var(--u));height:calc(140 * var(--u));}
/* width == the ball's diameter, so the flex box it occupies IS the wedge
   it sits in. Keep in step with `nestle` on the leaning book. */
.prop.tennis svg{width:calc(54 * var(--ub));height:calc(50 * var(--ub));}
/* the camera stands on its own flat book, centred on it: (246 − 134) / 2 */
.prop.camera{position:absolute;bottom:100%;left:calc(56 * var(--ub));z-index:2;}
.prop.camera svg{width:calc(134 * var(--ub));height:calc(102 * var(--ub));}
/* the shuttlecock stands ON the flat stack: bottom:100% puts its foot on
   the top book's cover, and fit() bottom-aligns the mesh to that same edge */
.prop.shuttle{position:absolute;bottom:100%;left:calc(74 * var(--ub));z-index:2;}
.prop.shuttle svg{width:calc(52 * var(--ub));height:calc(64 * var(--ub));}

/* The medal is hooked over the UPPER shelf — under the Projects easel,
   the last board in the row — and dangles into the bay below. `.hanger`
   is a zero-width flex item sitting right after that book, so flex
   arithmetic keeps the medal over the stand for free; `top:0` puts the
   ribbon's head exactly at the shelf surface and the drop hangs from there. */
.hanger{position:relative;flex:0 0 auto;width:0;height:0;align-self:flex-end;}
/* offset = gap (14) + half the Projects board (70) − half the ribbon (22),
   so the medal keeps hanging over that easel now the board is wider */
.prop.medal{position:absolute;top:0;right:calc(62 * var(--u));z-index:2;}
.prop.medal svg{width:calc(44 * var(--u));height:calc(152 * var(--u));}

/* the three spine-out volumes touch each other; the mementos beside
   them get the rest of the shelf */
.vols{display:flex;align-items:flex-end;flex:0 0 auto;}
/* Two clusters with one gap between them, not four even gaps — evenly
   spaced objects read as a specimen tray, grouped ones read as arranged
   by a person. */
.grp{display:flex;align-items:flex-end;flex:0 0 auto;gap:calc(24 * var(--ub));}
.row.sections .prop.trophy{margin-right:calc(12 * var(--u));}
.row.sections .prop.ball{margin-left:calc(14 * var(--u));}

/* the case is exactly as wide as its books — it can never be a
   half-empty shelf, because it has no width of its own */
.carcass{
  position:relative;
  width:max-content;max-width:100%;
  margin:0 auto;
  padding:0 15px;
  perspective:1150px;perspective-origin:50% 44%;
}
.carcass::after{
  content:"";position:absolute;inset:0;z-index:-1;pointer-events:none;
  box-shadow:calc(var(--sx) * .7) calc(var(--sy) * .7) 28px -8px rgb(var(--shade) / .28),
             0 3px 0 rgb(var(--shade) / .28);
}
.upright{
  position:absolute;top:0;bottom:0;width:15px;z-index:6;
  background:linear-gradient(90deg, var(--oak-shade) 0 1px, var(--oak-face) 1px 46%, var(--oak-sun) 100%);
}
.upright.l{left:0;}
.upright.r{right:0;background:linear-gradient(90deg, var(--oak-sun) 0 62%, var(--oak-face) 100%);}
.upright::after{
  content:"";position:absolute;inset:0;
  background-image:var(--fibre);background-size:100% 520px;
  opacity:.26;mix-blend-mode:multiply;
}

.board{
  position:relative;z-index:5;height:13px;
  background:linear-gradient(180deg,
    var(--oak-edge) 0 1.5px, var(--oak-sun) 1.5px 42%,
    var(--oak-face) 42% 72%, var(--oak-shade) 100%);
  box-shadow:0 1px 0 rgb(var(--shade) / .48), 0 4px 9px -3px rgb(var(--shade) / .38);
}
.board::after{
  content:"";position:absolute;inset:0;
  background-image:var(--fibre);background-size:520px 100%;
  opacity:.2;mix-blend-mode:multiply;
}
.board.top-board{height:15px;}
.board.plinth{height:17px;box-shadow:0 1px 0 rgb(var(--shade)/.48), 0 18px 28px -14px rgb(var(--shade) / .46);}

.bay{
  position:relative;
  background:linear-gradient(184deg, var(--bay-1), var(--bay-2));
  box-shadow:
    inset 0 4px 5px -3px rgb(var(--shade) / .78),
    inset 0 24px 32px -20px rgb(var(--shade) / .88),
    inset 20px 0 28px -20px rgb(var(--shade) / .78),
    inset -20px 0 28px -22px rgb(var(--shade) / .42),
    inset 0 -13px 22px -15px rgb(var(--shade) / .78);
}
.bay::before{
  content:"";position:absolute;inset:0;
  background-image:var(--fibre);background-size:340px 100%;
  opacity:.14;mix-blend-mode:multiply;
}
.bay::after{
  content:"";position:absolute;inset:0;pointer-events:none;
  background:radial-gradient(56% 80% at 94% 12%, rgb(var(--warm) / .18) 0%, transparent 62%);
}

.row{position:relative;z-index:2;display:flex;align-items:flex-end;justify-content:center;}

/* ── top shelf: the five you can open ───────────────────────── */
.row.sections{
  /* Above the shelf boards (z5). The medal's button hangs BELOW the shelf
     lip into the bay underneath, and rows sit at z2 — so the board and the
     DOM-later library row were swallowing every pointer event over it. The
     canvas paints the medal at z7 regardless, which is how it managed to
     LOOK hoverable while being dead to the mouse. */
  z-index:6;
  gap:calc(14 * var(--u));
  /* The two bays are the SAME height — a case with one deep bay and one
     shallow one reads as a mistake, not a design. Bottom bay is 16u of
     headroom over a 228u spine = 244u, so this one is 34 + 210. The
     headroom is larger here because these books lift 20u on hover and
     have to clear the board above. */
  padding:calc(34 * var(--u)) 8px 0;
  transform-style:preserve-3d;
  /* A preserve-3d parent is itself a plane at z=0, and it hit-tests there.
     Work and Writing sit at negative --z, i.e. BEHIND that plane, so the
     row was swallowing their clicks — they looked dead on hover while
     their forward neighbours worked. The row has no paint of its own, so
     it has no business being a pointer target. */
  pointer-events:none;
}
.row.sections .book,.row.sections .prop{pointer-events:auto;}

/* The button is a STATIC frame; only .boards moves. The easel used to be a
   child of the transformed element, so lifting the book carried its stand
   up with it — furniture doesn't follow the thing you pick up off it. */
.book{
  position:relative;flex:0 0 auto;
  /* The three dimensions arrive from the script as bare NUMBERS and are
     multiplied by the unit here, not inline, so a media query can restate
     one of them — an inline calc() would outrank every rule below. */
  --w:calc(var(--bw) * var(--u));
  --h:calc(var(--bh) * var(--u));
  --d:calc(var(--bd) * var(--u));
  width:var(--w);height:var(--h);
  transform-style:preserve-3d;
  --tilt:7deg;
  margin-left:var(--mx, 0px);
}
.boards{
  position:absolute;inset:0;
  transform-style:preserve-3d;
  /* face-out display: cover toward the room, resting back on its stand.
     --ry is a few degrees of variance, not a shelving angle; --tilt is the
     easel's recline. Depth comes from the tilt, the thickness stagger
     (--z) and the cast shadows, not from a big turn. */
  transform:translateY(calc(-7 * var(--u))) rotate(var(--lz, 0deg)) rotateY(var(--ry)) rotateX(var(--tilt)) translateZ(var(--z, 0px));
  transform-origin:50% 100%;
  transition:transform .44s cubic-bezier(.22,.9,.3,1);
  will-change:transform;          /* keeps the 3D box on one layer */
}
.book:hover .boards,.book:focus-visible .boards{
  /* picked up off the stand: straightens, comes to you, shows its edge */
  transform:translateY(calc(-20 * var(--u))) rotate(var(--lz, 0deg)) rotateY(calc(var(--ry) - 9deg)) rotateX(2deg) translateZ(calc(52 * var(--u)));
}
.face{position:absolute;left:50%;top:50%;backface-visibility:hidden;}

.f-cover{
  width:var(--w);height:var(--h);
  transform:translate(-50%,-50%) translateZ(calc(var(--d) / 2 + .5px));
  background:
    /* bumped corners on the fore-edge side — books get handled */
    radial-gradient(9% 8% at 99% 2%, rgb(var(--shade) / .3), transparent 70%),
    radial-gradient(9% 8% at 99% 98%, rgb(var(--shade) / .34), transparent 70%),
    /* one soft sheen only. Stacking a periwinkle wash AND a cream wash
       AND an inset glow over the cloth was reading as fog on glass —
       the cloth colour has to stay the loudest thing on the board. */
    linear-gradient(104deg, rgb(var(--shade) / .2) 0%, transparent 38%, rgb(255 252 240 / .05) 74%, rgb(255 252 240 / .1) 100%),
    /* the hinge groove where the board meets the spine */
    linear-gradient(90deg, rgb(var(--shade) / .38) 0, rgb(var(--shade) / .14) 1.5%, transparent 3%, transparent 4.5%, rgb(var(--shade) / .28) 6%, transparent 8.5%),
    /* No woven warp-and-weft here. A 1px repeating grid sells bookcloth at
       arm's length, but on a pale minimal board at 130px it stops being
       texture and becomes stripes. Smooth stock, and the grain overlay
       below carries what little tooth is wanted. */
    var(--cloth);
  border-radius:2px 5px 5px 2px;
  box-shadow:inset 0 0 22px rgb(var(--shade) / .12),
             inset 0 1px 0 rgb(255 252 240 / .2),
             inset -1px 0 0 rgb(255 252 240 / .14),
             inset 0 -1px 0 rgb(var(--shade) / .3),
             /* the book throws its own shadow onto the bay behind it */
             calc(var(--sx) * .9) calc(var(--sy) * .55) 16px -2px rgb(var(--shade) / .42);
  overflow:hidden;
}
/* The type rides its own plane, NOT the board's. Ink painted onto
   .f-cover is rasterised once and then resampled through the easel's
   rotateX/rotateY — every glyph edge smears by half a pixel and the
   whole series reads faintly out of focus. So the mark, title and
   numeral live on .f-type: a face held 3px off the cover and counter-
   rotated by exactly the board's angles, which keeps the type parallel
   to the screen (axis-aligned raster, crisp) while it inherits every
   board motion. The counter-rotation must mirror .boards IN BOTH STATES
   and share its transition timing — same curve, same duration — so the
   angles cancel frame-by-frame mid-flight; change one, change both. */
.f-type{
  width:var(--w);height:var(--h);
  /* Pivot at the bottom-RIGHT corner, not the centre. The counter-rotated
     plane is flat to the screen, so the whole face sits at its pivot's
     depth — and the board's bottom-right is its most FORWARD point in
     both rest and hover states: the easel recline (tilt > 0) brings the
     bottom toward the room, and ry < 0 turns the fore edge — the RIGHT
     side — toward the viewer (that's why each book shows its page block
     on the right). Pivot anywhere else and part of this plane lands
     behind the cover, where the 3D sorter clips the type mid-glyph. */
  transform-origin:100% 100%;
  transform:translate(-50%,-50%) translateZ(calc(var(--d) / 2 + 3px))
            rotateX(calc(-1 * var(--tilt))) rotateY(calc(-1 * var(--ry))) rotate(calc(-1 * var(--lz, 0deg)));
  transition:transform .44s cubic-bezier(.22,.9,.3,1);
  pointer-events:none;
  /* Minimal: one paper stock, one ink, and most of the board left empty.
     Everything sits on a single left margin and reads bottom-up — mark,
     rule, title, numeral. Left-aligned rather than centred, because
     centring everything is what makes a cover look like a default.

     Every measurement here is in --u, NOT in % of the board. Percentages
     made the margin, the mark and the type size all track each book's
     thickness, so five covers that should read as one series each had
     their own type scale and sat on five different baselines. */
  display:flex;flex-direction:column;align-items:flex-start;justify-content:flex-end;
  padding:0 calc(19 * var(--u)) calc(28 * var(--u));
}
.book:hover .f-type,.book:focus-visible .f-type{
  transform:translate(-50%,-50%) translateZ(calc(var(--d) / 2 + 3px))
            rotateX(-2deg) rotateY(calc(9deg - var(--ry))) rotate(calc(-1 * var(--lz, 0deg)));
}
.f-cover::after{
  content:"";position:absolute;inset:0;pointer-events:none;
  background-image:var(--grain);opacity:.06;mix-blend-mode:overlay;
}
.f-cover::before{   /* oak bouncing light back up into the boards */
  content:"";position:absolute;left:0;right:0;bottom:0;height:24%;
  pointer-events:none;z-index:1;
  background:linear-gradient(0deg, rgb(var(--bounce) / .18) 0%, transparent 100%);
  mix-blend-mode:screen;
}
/* one title, one rule, one numeral — the repeated byline on every
   cover was noise */
/* one hairline, no boxed band — the rule does the dividing that two heavy
   borders and a filled panel were doing before */
.f-type .band{
  position:relative;z-index:2;text-align:left;width:100%;
  padding:calc(9 * var(--u)) 0 0;
  border-top:1px solid color-mix(in srgb, var(--foil) 34%, transparent);
}
.f-type .band b{
  display:block;font-weight:400;
  font-size:calc(17 * var(--u));line-height:1.15;
  letter-spacing:-.004em;
}
/* The numeral read worst at 45% ink, so it holds more foil now — kept
   at 7.5u though; the fix is contrast, not size. */
.f-type .num{
  position:relative;z-index:2;margin-top:calc(6 * var(--u));
  font-family:var(--mono);
  font-size:calc(7.5 * var(--u));letter-spacing:.2em;
  text-transform:uppercase;
  color:color-mix(in srgb, var(--foil) 62%, transparent);
}

.f-spine{
  width:var(--d);height:var(--h);
  transform:translate(-50%,-50%) rotateY(-90deg) translateZ(calc(var(--w) / 2 + .5px));
  /* a backbone is convex with rounded head and tail caps, never a slab:
     bright ridge down the centre, corners that arc away */
  border-radius:32% / 7px;
  background:
    linear-gradient(90deg, rgb(var(--shade) / .66) 0%, rgb(var(--shade) / .2) 24%,
      rgb(255 250 236 / .24) 46%, rgb(var(--shade) / .26) 74%, rgb(var(--shade) / .54) 100%),
    var(--cloth);
  display:flex;align-items:center;justify-content:center;
  /* the spine faces AWAY from the window — a face at a different angle
     to the light must read a different value, or the box goes flat */
  filter:brightness(.72) saturate(1.08);
}
.f-spine em{
  writing-mode:vertical-rl;font-style:normal;
  font-size:calc(var(--d) * .3);letter-spacing:.05em;
  opacity:.82;white-space:nowrap;
}
/* the text block is inset from the boards — a hardcover's "square" */
.f-fore{
  width:calc(var(--d) - 5px);height:calc(var(--h) - 8px);
  transform:translate(-50%,-50%) rotateY(90deg) translateZ(calc(var(--w) / 2 - 2px));
  background:
    linear-gradient(90deg, rgb(var(--shade) / .32), transparent 40%),
    /* the block is slightly convex, and no two leaves sit flush */
    radial-gradient(120% 70% at 55% 50%, rgb(255 255 255 / .14), transparent 72%),
    repeating-linear-gradient(90deg, transparent 0 4px, rgb(var(--shade) / .07) 4px 5px, transparent 5px 9px),
    repeating-linear-gradient(90deg, #F3EEE0 0 1px, #D8D0BC 1px 2px, #EAE3D2 2px 3px);
  border-radius:1px;
}
.f-top{
  width:var(--w);height:var(--d);
  transform:translate(-50%,-50%) rotateX(90deg) translateZ(calc(var(--h) / 2 + .5px));
  background:
    repeating-linear-gradient(90deg, rgb(var(--shade) / .05) 0 1px, transparent 1px 3px),
    linear-gradient(180deg, var(--cloth) 0 9%, #EDE6D5 9% 91%, var(--cloth) 91% 100%);
  filter:brightness(.94);
}
/* the little easel: an oak ledge the book rests behind, feet on the
   shelf. This is what says "these five are on display on purpose". */
.stand{
  /* Sits on the shelf, NOT relative to the board it holds. It used to be
     dragged 8u below the shelf and only looked right because the whole
     book was lifted 7u on top of it; now that only .boards moves, the
     easel has to stand on the wood by itself. */
  position:absolute;left:50%;bottom:0;
  width:44%;height:calc(7 * var(--u));
  transform:translateX(-50%) translateZ(calc(var(--d) / 2 + 3 * var(--u))) rotateX(14deg);
  /* darker than the shelf it sits on, or it reads as a white slab */
  background:linear-gradient(180deg, var(--oak-sun) 0 18%, var(--oak-face) 18% 58%, var(--oak-shade) 100%);
  border-radius:2px;
  box-shadow:0 1px 0 rgb(var(--shade) / .5),
             calc(var(--sx) * .4) calc(6 * var(--u)) 8px -2px rgb(var(--shade) / .45);
  pointer-events:none;
}
.stand::before{  /* the two feet, peeking out under the ledge */
  content:"";position:absolute;left:6%;right:6%;bottom:calc(-4 * var(--u));
  height:calc(5 * var(--u));
  background:
    linear-gradient(90deg, var(--oak-face) 0 12%, transparent 12% 88%, var(--oak-face) 88% 100%);
  filter:brightness(.9);
}
.rest{
  position:absolute;left:-18%;right:-26%;top:100%;margin-top:calc(6 * var(--u));
  height:calc(15 * var(--u));
  background:radial-gradient(56% 100% at 44% 0%,
    rgb(var(--shade) / .88) 0%, rgb(var(--shade) / .36) 42%, transparent 76%);
  filter:blur(2.5px);transform:translateZ(-1px);
  transition:opacity .44s ease, transform .44s ease;
}
.book:hover .rest,.book:focus-visible .rest{opacity:.42;transform:translateZ(-1px) translateY(9px) scaleX(1.14);}

/* ── bottom shelf: the library ──────────────────────────────── */
.row.library{
  gap:0;padding:calc(16 * var(--u)) calc(26 * var(--u)) 0;
  justify-content:space-between;    /* books at one end, mementos spaced out */
  transform-style:flat;scrollbar-width:none;
}
.row.library::-webkit-scrollbar{display:none;}

.vol{
  position:relative;flex:0 0 auto;
  width:var(--w);height:var(--h);
  background:
    linear-gradient(90deg, rgb(var(--shade) / .58) 0%, rgb(var(--shade) / .14) calc(24% + var(--phase) * 1%),
      rgb(255 250 236 / .17) calc(46% + var(--phase) * 1.4%), rgb(var(--shade) / .2) 76%, rgb(var(--shade) / .5) 100%),
    var(--cloth);
  border-radius:2px 2px 1px 1px;
  transform:rotate(var(--lean, 0deg));
  transform-origin:var(--pivot, bottom left);
  box-shadow:inset 0 0 12px rgb(var(--shade) / .28);
  filter:brightness(calc(.96 + var(--sun) * .05)) saturate(calc(1.05 - var(--sun) * .11));
  transition:transform .3s cubic-bezier(.22,.9,.3,1);
  display:flex;align-items:center;justify-content:center;cursor:pointer;
}
.vol:hover,.vol:focus-visible{transform:rotate(var(--lean,0deg)) translateY(calc(-9 * var(--u)));}
.vol::before{
  content:"";position:absolute;z-index:2;pointer-events:none;
  top:0;bottom:0;left:-3px;width:6px;
  background:linear-gradient(90deg, transparent, rgb(var(--shade) / .7));
  filter:blur(1.5px);
}
.vol::after{
  content:"";position:absolute;inset:0;pointer-events:none;
  background-image:var(--grain);opacity:.11;mix-blend-mode:overlay;
}
.hub{
  position:absolute;left:0;right:0;height:3px;z-index:1;pointer-events:none;
  background:linear-gradient(180deg, rgb(255 250 236 / .18) 0 1px, rgb(var(--shade) / .38) 1px 100%);
}
.endband{
  position:absolute;left:18%;right:18%;height:2px;z-index:1;pointer-events:none;
  background:repeating-linear-gradient(90deg, var(--foil) 0 1px, rgb(var(--shade) / .5) 1px 2.5px);
  opacity:.24;
}
.endband.head{top:3px;} .endband.tail{bottom:3px;}

.vol .txt{
  writing-mode:vertical-rl;
  display:flex;flex-direction:row;align-items:center;gap:calc(13 * var(--u));
  max-height:88%;color:var(--foil);z-index:3;
}
.vol .t{
  font-size:clamp(10.5px, calc(var(--w) * .235), 16px);
  letter-spacing:.005em;white-space:nowrap;
  overflow:hidden;text-overflow:ellipsis;
}
/* A long title has to share the spine's HEIGHT with the author's name, so
   past ~15 characters it gets set smaller rather than ellipsised — a
   truncated title is worse than a slightly smaller complete one. */
.vol.longt .t{font-size:clamp(9.5px, calc(var(--w) * .185), 13px);}
.vol.longt .a{font-size:clamp(7.5px, calc(var(--w) * .13), 10px);}
.vol .a{
  font-size:clamp(8px, calc(var(--w) * .15), 11px);
  font-style:italic;opacity:.74;white-space:nowrap;
}
.vol.slim .a{display:none;}
.vol.slim .endband{display:none;}

/* ── books lying flat, so some titles read horizontally ─────── */
.stack{
  /* positioned: the shuttlecock is pinned to the top of this stack */
  position:relative;
  display:flex;flex-direction:column-reverse;align-items:flex-start;
  flex:0 0 auto;margin-left:calc(14 * var(--u));
}
.flat{
  position:relative;
  width:var(--fw);height:var(--fh);
  margin-left:var(--off, 0px);
  background:
    linear-gradient(180deg, rgb(255 250 236 / .2) 0%, rgb(var(--shade) / .1) 34%, rgb(var(--shade) / .46) 100%),
    var(--cloth);
  border-radius:1px 2px 2px 1px;
  box-shadow:0 1px 0 rgb(var(--shade) / .5), 0 2px 4px -2px rgb(var(--shade) / .5);
  display:flex;align-items:center;justify-content:space-between;
  padding:0 5%;gap:7px;
  color:var(--foil);cursor:pointer;
  transition:transform .3s cubic-bezier(.22,.9,.3,1);
}
.flat:hover,.flat:focus-visible{transform:translateX(calc(6 * var(--u)));}
.flat::after{
  content:"";position:absolute;inset:0;pointer-events:none;
  background-image:var(--grain);opacity:.1;mix-blend-mode:overlay;
}
.flat .t{
  font-size:clamp(9px, calc(var(--fh) * .40), 14px);
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis;
}
.flat .a{
  font-size:clamp(7.5px, calc(var(--fh) * .30), 11px);
  font-style:italic;opacity:.72;white-space:nowrap;flex:0 0 auto;
}
/* the fore edge of a book lying flat */
.flat::before{
  content:"";position:absolute;left:2px;right:2px;bottom:-1.5px;height:1.5px;
  background:repeating-linear-gradient(90deg,#F1ECDE 0 1px,#CFC6B0 1px 2px);
  border-radius:0 0 2px 2px;
}

/* ── materials & foil ───────────────────────────────────────────
   Four bindings, not one: cloth (default), leather, printed jacket,
   paperback. And every title is STAMPED, not painted — the foil is a
   gradient clipped to the glyphs, with one specular band placed by
   the same light that places the shadows. */
.f-type .band b,.f-spine em,.vol .t,.flat .t{
  /* Sheen, not chiaroscuro. Mixing the foil 25-40% toward brown at both
     ends looked like stamped gold in isolation but ate the letterforms on
     a spine 40px wide — the strokes went dark against dark cloth. The
     darkest stop is now within 10% of the foil, so the gradient reads as
     a highlight passing over type that stays at full contrast. */
  background-image:linear-gradient(105deg,
    color-mix(in srgb, var(--foil) 90%, #2F2818) 0%,
    var(--foil) 30%,
    color-mix(in srgb, var(--foil) 68%, #FFFDF2) 50%,
    var(--foil) 70%,
    color-mix(in srgb, var(--foil) 88%, #2F2818) 100%);
  -webkit-background-clip:text;background-clip:text;color:transparent;
  filter:drop-shadow(0 1px 1px rgb(var(--shade) / .55));
}

/* leather: one deep sheen instead of woven light, heavier tooth,
   blind-tooled double rules around the title */
.book.m-leather .f-cover{
  background:
    radial-gradient(10% 9% at 99% 2%, rgb(var(--shade) / .4), transparent 70%),
    radial-gradient(10% 9% at 99% 98%, rgb(var(--shade) / .44), transparent 70%),
    linear-gradient(104deg, rgb(var(--shade) / .3) 0%, transparent 40%, rgb(255 248 235 / .1) 72%, rgb(255 248 235 / .2) 100%),
    linear-gradient(90deg, rgb(var(--shade) / .38) 0, rgb(var(--shade) / .14) 1.5%, transparent 3%, transparent 4.5%, rgb(var(--shade) / .28) 6%, transparent 8.5%),
    radial-gradient(130% 100% at 68% 18%, color-mix(in srgb, var(--cloth) 86%, #FFF3E0) 0%, var(--cloth) 52%, color-mix(in srgb, var(--cloth) 68%, #170F0B) 100%);
  border-radius:2px 4px 4px 2px;
  box-shadow:inset 0 0 26px rgb(23 14 10 / .42),
             inset 0 1px 0 rgb(255 244 228 / .25),
             inset -1px 0 0 rgb(255 244 228 / .18);
}
.book.m-leather .f-cover::after{opacity:.17;}
.book.m-leather .band{
  border-top:3px double color-mix(in srgb, var(--foil) 55%, transparent);
  border-bottom:3px double color-mix(in srgb, var(--foil) 55%, transparent);
}

/* printed jacket: smooth paper, a gloss streak, and the flap crease
   a wrapped jacket always carries near the fore edge */
.book.m-jacket .f-cover{
  background:
    linear-gradient(104deg, rgb(var(--shade) / .18) 0%, transparent 34%, rgb(255 255 255 / .2) 66%, rgb(255 255 255 / .32) 100%),
    linear-gradient(90deg, transparent 86.5%, rgb(var(--shade) / .2) 88%, rgb(255 255 255 / .28) 89.5%, transparent 91.5%),
    linear-gradient(90deg, rgb(var(--shade) / .32) 0, transparent 3%),
    linear-gradient(180deg, color-mix(in srgb, var(--cloth) 90%, #FFFDF4) 0%, var(--cloth) 55%, color-mix(in srgb, var(--cloth) 88%, #241B14) 100%);
  box-shadow:inset 0 0 12px rgb(var(--shade) / .18), inset 0 1px 0 rgb(255 255 255 / .4);
}
.book.m-jacket .f-cover::after{opacity:.05;}

/* ── the five covers: one monochrome series ─────────────────────
   A small centred pictogram over a wash is the shape every templated
   card takes, so these are built like a publisher's series instead:
   ONE ink, ONE paper, alternating which is the ground, a bold graphic
   that bleeds off the trimmed edge, and a solid title band that the
   art runs behind. Nothing is decorative-thin — every mark is a filled
   plane or a heavy rule, because at 130px a hairline is a scratch. */
.f-type .art{
  /* a small precise mark on the same margin as the type, held high so the
     empty middle of the board does the work */
  position:absolute;left:calc(19 * var(--u));top:calc(30 * var(--u));
  width:calc(38 * var(--u));
  z-index:1;pointer-events:none;
}
.f-type .art svg{display:block;width:100%;height:auto;}
/* the band carries its own ground, so the title stays legible whatever
   the artwork does behind it */
/* Monochrome series: the title is INKED, not foil-stamped. The metallic
   gradient that sells gold on cloth turns muddy the moment the two tones
   are ink and paper, so it is switched off here. */
.book.mono .f-type .band b,.book.mono .f-spine em{
  background-image:none;
  -webkit-background-clip:border-box;background-clip:border-box;
  color:var(--foil);filter:none;
}
.book.mono .f-cover{box-shadow:
  inset 0 0 22px rgb(var(--shade) / .1),
  inset 0 1px 0 rgb(255 252 240 / .14),
  calc(var(--sx) * .9) calc(var(--sy) * .55) 16px -2px rgb(var(--shade) / .42);}

/* a pasted paper label — the classic way a dark spine carries a
   legible title */
.book.label .band{
  background:linear-gradient(174deg, #F6F0DD, #E7DEC6);
  border:1px solid rgb(var(--shade) / .4);
  border-radius:1px;padding:8% 9%;
  box-shadow:0 1px 2px rgb(var(--shade) / .4), inset 0 1px 0 #FFFDF4;
}
.book.label .band b{
  background-image:linear-gradient(#3A342A, #3A342A);
  filter:none;
}

/* library volumes: l leather, j jacket, p paperback (cloth default) */
.vol.l{
  background:
    linear-gradient(90deg, rgb(var(--shade) / .62) 0%, rgb(var(--shade) / .16) calc(24% + var(--phase) * 1%),
      rgb(255 246 230 / .2) calc(46% + var(--phase) * 1.4%), rgb(var(--shade) / .22) 76%, rgb(var(--shade) / .55) 100%),
    radial-gradient(120% 100% at 50% 0%, color-mix(in srgb, var(--cloth) 90%, #FFF8EC) 0%, var(--cloth) 45%, color-mix(in srgb, var(--cloth) 72%, #150F0A) 100%);
  box-shadow:inset 0 0 14px rgb(20 12 8 / .35);
}
.vol.l::after{opacity:.17;}
.vol.l .hub{
  height:4px;border-radius:2px;
  background:linear-gradient(180deg, rgb(255 250 236 / .32) 0 1px, rgb(var(--shade) / .45) 1px 100%);
}
.fillet{
  position:absolute;left:10%;right:10%;height:1px;z-index:1;pointer-events:none;
  background:var(--foil);opacity:.4;
}
.vol.j{box-shadow:inset 0 0 9px rgb(var(--shade) / .2);}
.vol.j::after{opacity:.05;}
.sheen{
  position:absolute;inset:0;z-index:2;pointer-events:none;
  background:linear-gradient(90deg, transparent 34%, rgb(255 255 255 / .16) 46%, transparent 60%);
}
.vol.p{
  background:
    linear-gradient(90deg, rgb(var(--shade) / .5) 0%, rgb(var(--shade) / .12) calc(24% + var(--phase) * 1%),
      rgb(255 250 236 / .14) calc(46% + var(--phase) * 1.4%), rgb(var(--shade) / .16) 76%, rgb(var(--shade) / .44) 100%),
    /* the one crease every read paperback carries */
    linear-gradient(90deg, transparent 30%, rgb(var(--shade) / .24) 31%, transparent 32.5%),
    var(--cloth);
  border-radius:1px;
  box-shadow:inset 0 0 8px rgb(var(--shade) / .2);
}
.vol.p::after{opacity:.05;}
.vol.p .hub,.vol.p .endband{display:none;}
.colo{
  position:absolute;left:50%;bottom:5.5%;z-index:1;pointer-events:none;
  width:calc(6 * var(--ub));height:calc(6 * var(--ub));
  transform:translateX(-50%) rotate(45deg);
  background:var(--foil);opacity:.45;
}

.shelf-note{
  position:relative;z-index:3;
  display:flex;justify-content:space-between;align-items:baseline;gap:16px;
  padding:11px 3px 0;
  font-size:13.5px;font-style:italic;color:var(--ink-3);
}
.shelf-note .n{font-style:normal;font-family:var(--mono);font-size:11.5px;font-variant-numeric:tabular-nums;}

/* ═══ THE OPENED BOOK ═════════════════════════════════════════ */
.scrim{
  position:fixed;inset:0;z-index:60;
  background:rgb(43 46 58 / .48);
  backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px);
  display:flex;align-items:center;justify-content:center;padding:26px 18px;
  opacity:0;pointer-events:none;transition:opacity .3s ease;
}
.scrim.on{opacity:1;pointer-events:auto;}
.spread{
  /* The frame fits the chapter, not one size for all: each book sets
     --pw (page width) in its data — About is five lines and keeps a
     narrow page, Projects carries full-measure plates and gets a wide
     one. Height needs no per-page number: the card is only as tall as
     its content, up to the viewport cap. */
  position:relative;width:min(var(--pw, 680px),100%);max-height:90vh;
  background:linear-gradient(100deg, #EFEDE6 0%, var(--paper) 6%, var(--paper-warm) 100%);
  color:var(--ink);border-radius:2px 5px 5px 2px;
  box-shadow:-13px 0 20px -17px rgb(var(--shade) / .9) inset,
             0 32px 62px -26px rgb(var(--shade) / .68);
  display:flex;flex-direction:column;overflow:hidden;
  transform:translateY(12px) scale(.988);
  transition:transform .34s cubic-bezier(.22,.9,.3,1);
}
.scrim.on .spread{transform:none;}
.spread::before{
  content:"";position:absolute;top:5px;bottom:5px;right:-4px;width:4px;
  background:repeating-linear-gradient(180deg,#F3EEE0 0 1px,#D8D0BC 1px 2px);
  border-radius:0 3px 3px 0;
}
.spread-scroll{
  flex:1 1 auto;min-height:0;
  overflow-y:auto;overscroll-behavior:contain;
  padding:clamp(26px,3.6vw,40px) clamp(26px,4vw,48px) 16px clamp(34px,5vw,58px);
  /* the rail stays VISIBLE when a page overflows — a long reading list
     has to look long, and macOS overlay bars hid the only cue */
  scrollbar-width:thin;
  scrollbar-color:color-mix(in srgb, var(--ink-3) 46%, transparent) transparent;
}
.spread-scroll::-webkit-scrollbar{width:8px;}
.spread-scroll::-webkit-scrollbar-thumb{background:color-mix(in srgb, var(--ink-3) 40%, transparent);border-radius:4px;}
.spread-scroll::-webkit-scrollbar-track{background:transparent;}
.shut{
  position:absolute;top:12px;right:14px;z-index:4;
  width:30px;height:30px;border-radius:50%;
  display:flex;align-items:center;justify-content:center;
  font-size:16px;line-height:1;color:var(--ink-3);
  transition:background .18s,color .18s;
}
.shut:hover{background:rgb(34 38 47 / .07);color:var(--ink);}

/* ── the page is TYPESET, not templated ─────────────────────────
   chip + left title + hairline + floating grey paragraphs is the
   shape every generated modal takes. A book page has a ritual
   instead: a centred chapter head over a small ornament, a drop
   cap, justified text with indented paragraphs and no blank lines
   between them, a closing mark, a running foot. Same content,
   set the way the room implies it would be. */

/* the ribbon marker: hangs over the head of the page from behind
   the top edge, silk-shaded, coloured per chapter by the script */
.ribbon{
  position:absolute;top:0;right:58px;z-index:3;pointer-events:none;
  width:20px;height:92px;
  background:var(--cypress);
  clip-path:polygon(0 0,100% 0,100% 100%,50% calc(100% - 11px),0 100%);
  filter:drop-shadow(0 3px 5px rgb(var(--shade) / .35));
}
.ribbon::after{
  content:"";position:absolute;inset:0;
  background:linear-gradient(105deg, rgb(255 255 255 / .3) 0%, transparent 38%, rgb(var(--shade) / .25) 100%);
}

.chap{
  margin:2px 0 8px;text-align:center;
  font-size:14.5px;font-style:italic;letter-spacing:.045em;color:var(--ink-3);
}
.spread h2{
  margin:0;text-align:center;
  font-size:clamp(29px,3.6vw,36px);font-weight:400;letter-spacing:-.014em;
}
/* the ornament under the title — two short rules meeting a diamond */
.hr{
  display:flex;align-items:center;gap:11px;
  width:168px;height:auto;margin:15px auto 27px;
  background:none;
  font-size:8px;color:var(--cypress);opacity:.62;
}
.hr::before,.hr::after{content:"";flex:1;height:1px;background:var(--rule);}

/* These pages are for SCANNING, not for reading through. Prose is the
   exception here and lists are the rule, so nothing is justified and
   nothing is hyphenated — a ragged right edge is what lets the eye
   drop straight down a list. No drop cap either: it needs three lines
   of text under it to sit in, and most of these pages open with one. */
.spread p{margin:0 0 13px;font-size:18px;line-height:1.7;color:var(--ink-2);}
.spread p:last-child{margin-bottom:0;}
/* a link on a page is inked and underlined the way the masthead does
   it — no button dressing inside a book */
.spread a{
  color:var(--cypress);
  text-decoration:underline;
  text-decoration-color:rgb(42 93 83 / .34);
  text-decoration-thickness:1px;
  text-underline-offset:3px;
  transition:text-decoration-color .18s;
}
.spread a:hover{text-decoration-color:var(--cypress);}
#chapBody>p{margin:0 0 15px;font-size:18.5px;line-height:1.62;color:var(--ink);}
#chapBody>p:last-child{margin-bottom:0;}
/* a page with nothing on it yet says so quietly, centred, out of the
   way of the list that will replace it */
#chapBody>p.note{
  margin:2px 0 0;text-align:center;
  font-size:17px;font-style:italic;color:var(--ink-3);
}

/* ── the list, set the way a book sets one ──────────────────────
   Not a web bullet: the mark is the same small cypress diamond as the
   ornament under the title, it hangs in the MARGIN, and the text keeps
   one clean left edge when a point wraps to a second line. */
.pts{list-style:none;margin:0;padding:0;}
.pts>li{
  position:relative;
  margin:0 0 11px;padding-left:1.45em;
  font-size:18px;line-height:1.58;color:var(--ink);
}
.pts>li:last-child{margin-bottom:0;}
.pts>li::before{
  content:"";position:absolute;left:.28em;top:.62em;
  width:5px;height:5px;
  background:var(--cypress);opacity:.68;
  transform:rotate(45deg);
}
/* a second level for the odd sub-point: a rule, not a diamond, so the
   two levels never read as the same rank */
.pts .pts{margin:9px 0 0;}
.pts .pts>li{font-size:17px;color:var(--ink-2);margin-bottom:7px;}
.pts .pts>li::before{
  top:.78em;width:6px;height:1px;
  background:var(--ink-3);opacity:.5;transform:none;
}

/* when one page holds several kinds of thing, this groups them.
   #chapBody-qualified for the same reason .note is: the bare-paragraph
   blanket above carries an id and outranks a lone class — any classed
   <p> that lives directly in the body needs this rank to dress itself */
#chapBody .sub-h{
  margin:27px 0 13px;text-align:center;scroll-margin-top:10px;
  font-size:14.5px;font-style:italic;letter-spacing:.05em;color:var(--cypress);
}
#chapBody .sub-h:first-child{margin-top:0;}
/* the chapter closes the way chapters do */
#chapBody::after{
  content:"◆";display:block;
  margin:30px 0 2px;text-align:center;
  font-size:7px;color:var(--cypress);opacity:.55;
}

/* the running foot at the bottom of the page, outside the scroll; the
   fade above it is the scroll affordance — text sinks into the paper
   instead of guillotining at the folio */
.folio{
  position:relative;margin:0;padding:9px 0 12px;flex:0 0 auto;
  text-align:center;
  font-size:12.5px;font-style:italic;letter-spacing:.07em;color:var(--ink-3);
}
/* the fade repeats the page's own paper gradient and masks it in
   vertically — a flat colour here leaves a seam on the cooler side */
.folio::before{
  content:"";position:absolute;left:0;right:0;bottom:100%;height:30px;
  background:linear-gradient(100deg, #EFEDE6 0%, var(--paper) 6%, var(--paper-warm) 100%);
  -webkit-mask-image:linear-gradient(180deg, transparent, #000);
  mask-image:linear-gradient(180deg, transparent, #000);
  pointer-events:none;
}

/* entries are set like a table of contents: the title, a dotted leader
   out to the year, then its points bulleted underneath */
.rows{display:flex;flex-direction:column;gap:21px;}
#chapBody>*+.rows{margin-top:19px;}
.entry{display:flex;flex-wrap:wrap;align-items:baseline;}
.entry .t{
  flex:1;min-width:0;display:flex;align-items:baseline;
  font-size:23px;letter-spacing:-.01em;color:var(--ink);text-decoration:none;
}
.entry .t::after{
  content:"";flex:1;min-width:26px;margin:0 10px;
  border-bottom:1px dotted color-mix(in srgb, var(--ink-3) 52%, transparent);
  transform:translateY(-4px);
}
/* a linked title is not shy about it: it wears the site's link green,
   the way every clickable thing here does, and external ones carry a
   small compass arrow that leans into the corner on hover */
.entry a.t{color:var(--cypress);}
.entry a.t:hover{color:color-mix(in srgb, var(--cypress) 72%, black);}
.entry .t .ext{
  flex:0 0 auto;margin-left:7px;
  font-size:15px;font-style:normal;opacity:.75;
  transform:translateY(-2px);transition:transform .18s;
}
.entry a.t:hover .ext{transform:translate(2px,-4px);}
.entry .yr{font-family:var(--mono);font-variant-numeric:tabular-nums;font-size:13.5px;color:var(--ink-3);white-space:nowrap;}
/* the org/place line under a role, before its points */
.entry .org{flex-basis:100%;margin:2px 0 0;font-size:16px;font-style:italic;color:var(--ink-2);}
.entry .d{flex-basis:100%;margin:4px 0 0;padding-left:1.45em;font-size:16.5px;line-height:1.58;color:var(--ink-2);}
/* the reading order inside an entry is fixed: title line, then the
   points right under it, then the plate. Text never shares a line
   with media, so nothing squeezes at any width. */
.entry .pts{flex:1 1 100%;min-width:0;margin-top:8px;}
.entry .pts>li{font-size:17px;margin-bottom:7px;color:var(--ink-2);}
.entry .pts>li::before{top:.6em;width:4.5px;height:4.5px;}
/* a plate for a project — a thumbnail or a short clip framed like a
   tipped-in photograph. NOT full measure: a plate pasted into a book
   sits smaller than the text block, centred, with margin around it.
   Inside a flex .entry the .plate takes a full row AFTER the points
   (order:5), so the eye reads title then description, and only then
   meets the clip, centred and small, the way a book closes a section
   with its figure. */
.entry .plate{flex:0 0 100%;order:5;display:flex;justify-content:center;margin:12px 0 2px;}
.entry .plate .shot{width:min(420px,100%);margin:0;}
.shot{
  display:block;width:min(440px,100%);margin:12px auto 4px;
  border:1px solid var(--rule);border-radius:2px;
  box-shadow:0 2px 12px -7px rgb(var(--shade) / .55);
}
/* an empty plate: holds the frame and the space until the real still
   or film is dropped in — then the div becomes the img/video */
.shot.ph{
  aspect-ratio:16/9;
  display:flex;align-items:center;justify-content:center;
  background:
    linear-gradient(100deg, rgb(var(--shade) / .07), rgb(var(--shade) / .02) 55%, rgb(var(--shade) / .08)),
    var(--paper-warm);
  color:var(--ink-3);font-style:italic;font-size:15px;letter-spacing:.02em;
}

/* the reading list is a bibliography: no rules between works, the
   note hung on the same indent as everything else */
.bk{margin-top:17px;}
.bk:first-of-type{margin-top:0;}
.nowbox+.bk{margin-top:0;}
/* a cover beside a book: a small plate in its own margin column, the
   text keeping one clean edge. Covers live in covers/<isbn>.jpg IN THE
   REPO — they hotlinked Open Library at first and the third-party wait
   made every open feel slow; onerror drops a missing plate cleanly. */
.cov{
  display:block;
  border:1px solid var(--rule);border-radius:2px;
  box-shadow:0 2px 8px -5px rgb(var(--shade) / .55);
}
.bk.cv{display:grid;grid-template-columns:auto 1fr;column-gap:16px;align-items:start;}
.bk.cv .cov{grid-row:1 / span 3;width:64px;}
.bk.cv .bk-n{padding-left:0;}
/* two columns keep ten-plus books from burying the papers below them;
   each entry is short enough that the eye still reads down, not across */
.bks{display:grid;grid-template-columns:1fr 1fr;gap:22px 30px;}
.bks .bk{margin-top:0;}
/* the current read gets its cover as a frontispiece over the epigraph */
.nowbox .cov{width:76px;margin:10px auto 0;}

/* ── the keepsake cards ─────────────────────────────────────────
   The six objects don't get chapters — they get a museum label:
   the object ITSELF drawn on the card (openBook clones the prop's
   fallback SVG into #relic, so the art costs nothing), the name
   under it, a few short centred lines, and the last line allowed a
   wink in cypress. No ornament — the relic is the ornament. The
   .fun class flips on for anything without a chapter number. */
.relic{display:none;}
.spread.fun .relic{display:block;margin:10px auto 2px;text-align:center;}
/* overflow stays visible like the shelf originals — the drawings hang
   their ground shadows outside the viewBox, and clipping them flattened
   the ball's bottom edge */
.spread.fun .relic svg{height:104px;width:auto;overflow:visible;}
.spread.fun h2{font-size:clamp(23px,2.8vw,28px);}
.spread.fun .hr{display:none;}
.spread.fun .spread-scroll{padding-top:22px;}
.spread.fun #chapBody{margin-top:20px;}
#chapBody .fact{margin:0 0 13px;text-align:center;font-size:19px;line-height:1.6;color:var(--ink);}
#chapBody .fact.kick{margin-top:19px;font-style:italic;font-size:18px;color:var(--cypress);}
/* travel snaps on the photography card: small plates, each with a
   place line under it, waiting for the real pictures */
.snaps{display:grid;grid-template-columns:1fr 1fr;gap:16px 14px;margin:20px 0 4px;}
.snaps figure{margin:0;}
.snaps .shot{margin:0;width:100%;}
.snaps .shot.ph{aspect-ratio:4/3;font-size:13.5px;}
.snaps figcaption{margin-top:6px;text-align:center;font-size:13.5px;font-style:italic;color:var(--ink-3);}
/* the page's own contents line — one quiet row under the epigraph
   that jumps a long bibliography without scrolling through it */
#chapBody .toc{margin:0 0 22px;text-align:center;font-size:14px;font-style:italic;color:var(--ink-3);}
.bk-head{display:flex;justify-content:space-between;align-items:baseline;gap:14px;}
.bk-t{font-size:19.5px;}
.bk-a{font-style:italic;font-size:17px;color:var(--ink-2);}
.bk-n{margin:4px 0 0;padding-left:1.55em;font-size:17.5px;line-height:1.58;color:var(--ink-2);}
.rate{display:inline-flex;gap:3px;flex:0 0 auto;padding-top:.44em;}
.rate i{width:6px;height:6px;background:var(--cypress);display:block;}
.rate i.off{background:var(--rule);}

/* what's open on the desk right now, set as an epigraph — centred,
   italic note — rather than a bordered callout */
.nowbox{margin:0 auto 30px;max-width:46ch;text-align:center;}
.nowbox .k{font-size:14.5px;font-style:italic;color:var(--cypress);letter-spacing:.03em;}
.nowbox .bk-t{display:block;margin-top:4px;font-size:20.5px;}
.nowbox .bk-n{padding-left:0;font-style:italic;}

.vignette{
  position:fixed;inset:-2%;pointer-events:none;z-index:80;mix-blend-mode:multiply;
  background:radial-gradient(122% 96% at 56% 38%, transparent 56%, rgb(var(--shade) / .26) 100%);
}

/* ═══ RESPONSIVE ══════════════════════════════════════════════ */
/* the masthead runs proportionally wider from here down, so the bubble
   ducks below its last line — into the empty wall beside the cat's head
   rather than above its back */
/* ── the recommendation slip and its credit lines ─────────────────
   The slip is a small card page (opened by the cat or the Reading
   book); fields are ruled lines on the paper, nothing boxed. The
   credit line is how a visitor signs the book they left. */
.slip{max-width:380px;margin:18px auto 4px;}
.slip-f{
  display:block;margin:0 0 14px;
  font-size:13.5px;font-style:italic;letter-spacing:.03em;color:var(--ink-3);
}
.slip-f input{
  display:block;width:100%;margin-top:3px;padding:4px 2px 7px;
  font:17px var(--serif);font-style:normal;color:var(--ink);
  background:transparent;border:0;border-bottom:1px solid var(--rule);
  border-radius:0;outline:none;transition:border-color .18s;
}
.slip-f input:focus{border-bottom-color:var(--cypress);}
.slip-f input::placeholder{font-style:italic;color:var(--ink-3);opacity:.55;}
.slip-go{
  display:block;margin:22px auto 2px;padding:9px 24px;
  font:italic 15px var(--serif);letter-spacing:.04em;
  color:var(--paper-warm);background:var(--cypress);
  border:0;border-radius:2px;cursor:pointer;
  box-shadow:0 10px 18px -12px rgb(var(--shade) / .8);
  transition:filter .18s;
}
.slip-go:hover{filter:brightness(1.14);}
/* On-the-list: index entries of exactly two lines (title/leader/credit,
   then the why, truncated) grouped five to a page. Pages sit side by
   side in a strip that slides horizontally — swipe, or the arrows
   under it — so the section holds one height however many arrive. */
#chapBody .trs{
  max-width:640px;margin:0 auto;
  display:grid;grid-auto-flow:column;grid-auto-columns:100%;column-gap:48px;
  overflow-x:auto;scroll-snap-type:x mandatory;overscroll-behavior-x:contain;
  scrollbar-width:none;
}
#chapBody .trs::-webkit-scrollbar{display:none;}
#chapBody .tr-page{scroll-snap-align:start;min-width:0;}
#chapBody .tr{margin:0 0 11px;min-width:0;}
#chapBody .tr-row{display:flex;align-items:baseline;min-width:0;}
#chapBody .tr-t{font-size:17.5px;color:var(--ink);}
#chapBody .tr-a{margin-left:9px;font-size:13.5px;font-style:italic;color:var(--ink-3);white-space:nowrap;}
#chapBody .tr-lead{flex:1;min-width:26px;margin:0 10px;border-bottom:1px dotted color-mix(in srgb, var(--ink-3) 52%, transparent);transform:translateY(-4px);}
#chapBody .tr-c{font-size:13.5px;font-style:italic;color:var(--ink-3);white-space:nowrap;max-width:45%;overflow:hidden;text-overflow:ellipsis;}
#chapBody .tr-c a{color:var(--cypress);text-decoration:none;}
#chapBody .tr-c a:hover{text-decoration:underline;}
#chapBody .tr-w{
  margin:2px 0 0 14px;font-size:13.5px;font-style:italic;color:var(--ink-3);
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis;
}
/* the upvote: the triangle everyone knows, and a count, at the end of
   the line. One per book per browser; .on marks a vote already cast. */
#chapBody .tr-v{
  flex:0 0 auto;margin-left:10px;padding:0 2px;
  display:inline-flex;align-items:baseline;gap:4px;
  background:none;border:0;cursor:pointer;
  color:var(--ink-3);transition:color .15s;
}
#chapBody .tr-v i{font-style:normal;font-size:9px;opacity:.55;transform:translateY(-1px);}
#chapBody .tr-v b{font-family:var(--mono);font-weight:400;font-size:12.5px;font-variant-numeric:tabular-nums;}
#chapBody .tr-v:hover{color:var(--cypress);}
#chapBody .tr-v.on{color:var(--cypress);}
#chapBody .tr-v.on i{opacity:1;}
/* the page-turn: two quiet arrows and the count between them */
#chapBody .tr-nav{margin:4px 0 0;text-align:center;font-size:14px;}
#chapBody .tr-nav a{color:var(--cypress);text-decoration:none;padding:0 10px;}
#chapBody .tr-nav a:hover{text-decoration:underline;}
#chapBody .tr-nav .n{font-size:12.5px;font-style:italic;color:var(--ink-3);}
/* the quiet door to the slip at the foot of the On-the-list section */
#chapBody .tr-add{margin:16px 0 0;text-align:center;font-size:14.5px;font-style:italic;}
#chapBody .tr-add a{color:var(--cypress);text-decoration:none;}
#chapBody .tr-add a:hover{text-decoration:underline;}

/* ═══ THE DESKTOP ROOM AT THREE-QUARTERS ═══════════════════════
   The case is pinned at .75 of canonical (the --u ceiling up top), so
   on a desktop the room sits in the wall with air around it instead of
   filling the window. Everything measured in --u rescales itself; what
   follows are the PX-SIZED furnishings, stepped down by the same .75
   so the masthead doesn't loom over a smaller case. Gated ABOVE the
   tablet rules: below 961px the case is already derived smaller than
   .75 and the existing responsive sizes are the right ones. */
@media (min-width:961px){
  h1{font-size:clamp(22px,2.63vw,31px);}
  .sub{margin:8px 0 0;font-size:clamp(11.5px,.94vw,12.5px);}
  .sub .who{font-size:8.5px;}
  .sub.defn .word{font-size:13.5px;}
  .sub.defn .pron{font-size:9px;}
  .meta{margin:9px 0 0;font-size:10.5px;}
  .meta .sep{margin:0 5px;}
  .masthead{max-width:min(46ch, calc(100% - 175px));}
  .top{margin-bottom:clamp(9px,1.35vh,17px);}
  .sunpatch{width:min(300px,30vw);height:min(270px,28.5vw);}
  .window{width:clamp(99px,10.5vw,140px);}
  .win-bars i.v{left:calc(50% - 2px);width:4px;}
  .win-bars i.h{top:calc(46% - 2px);height:4px;}
  .win-frame{inset:-8px -8px -7px;border-width:8px;border-bottom-width:7px;}
  .win-sill{height:7px;margin:8px -14px 0;}
  .dream{font-size:12px;padding:7px 12px 8px;border-radius:14px 14px 14px 5px;}
  .dream::before{width:8px;height:8px;right:-9px;bottom:-7px;}
  .dream::after{width:4px;height:4px;right:-15px;bottom:-13px;}
  .shelf-note{font-size:10px;padding:8px 3px 0;}
  .shelf-note .n{font-size:8.5px;}
  /* the opened book scales as one piece — no canvas inside, so zoom
     is safe here (the room itself must NOT be zoomed: gl.js sizes its
     canvas from getBoundingClientRect and zoom would double-shrink it) */
  .spread{zoom:.75;}
}
@media (max-width:960px){
  .dream{bottom:calc(var(--catw) * .38 - 32px);font-size:13.5px;padding:7px 13px 8px;}
}
@media (max-width:820px){
  .window{width:min(148px,34%);}
  .masthead{max-width:calc(100% - 170px);}
  .sunpatch{top:16%;right:3%;width:54vw;height:46vw;}
  .carcass{perspective:850px;padding:0 11px;}
  .upright{width:11px;}
  .book{--tilt:5deg;}
  .spread-scroll{padding:24px 22px 14px 26px;}
  .bks{grid-template-columns:1fr;}
}
@media (max-width:660px){
  /* Phones drop the CAST, not just the scale. Squeezing the full case into
     360px needs --u near 0.3, which puts every spine label under its own
     legibility floor and then overflows the book it is set in — that is
     why this used to pin the unit and let the lower shelf scan sideways,
     which in turn meant most of the shelf sat off-screen behind a
     scrollbar. Instead: hide the decorative half of the lower shelf and
     size what remains to actually fit.

     Both units stay DERIVED so every width between 320 and 660 works,
     rather than only the two the media queries name. Divisor = the widest
     surviving row in units (752 up top: five boards plus four gaps; ~505
     below). Subtrahend covers every fixed pixel around it — 40px body
     padding, 22px carcass, 16px row — plus ~10px of slack; leaving those
     out is exactly how the top shelf ended up clipped. Measured, not
     estimated: 752 units is five boards (696) plus four gaps (56). */
  :root{
    --u:min(.8px, calc((100vw - 88px) / 752));
    --ub:min(1px, calc((100vw - 90px) / 520));
  }
  body{justify-content:flex-start;padding-top:22px;}
  /* the two display pieces would overflow the case at this width, the
     way the flanking books used to — the top shelf keeps its books */
  .row.sections .prop{display:none;}
  /* and the thought bubble goes with them: it was sitting on the masthead
     links, and most of the objects it advertises are hidden here anyway */
  .dream{display:none;}
  /* and the medal's hanger goes with them: it is zero-width but it still
     earns a flex gap, which is enough to push the row over */
  .hanger{display:none;}
  /* Here the two shelves are measured in DIFFERENT units — the spines keep
     --ub so their labels stay readable — so equal bays can't come from one
     constant the way they do on desktop. Five boards have to share 302px
     at 390, which makes them short enough to leave a band of dead air
     under the shelf above; the boards take some of it back in height
     (the width is what's constrained here, not the height), and the rest
     becomes headroom. Lower bay is 16u + 228ub — its padding plus Don
     Quixote — so this one is that, less its own 258u board. */
  .book{--tilt:4deg;--h:calc(258 * var(--u));}
  .row.sections{padding-top:calc(228 * var(--ub) - 242 * var(--u));}
  /* the reduced cast: three spines, the ball, and the camera on its book */
  .hide-sm{display:none;}
  /* a phone page is too narrow for text beside a figure — the plate
     drops under its points, centred */
  .entry .plate{flex:1 1 100%;margin:12px auto 0;max-width:400px;}
  .row.library{
    padding:calc(16 * var(--u)) calc(12 * var(--u)) 0;
    /* a safety net only — the content above is sized to fit without it */
    overflow-x:auto;overflow-y:hidden;
    -webkit-overflow-scrolling:touch;overscroll-behavior-x:contain;
  }
  .stack{margin-left:calc(8 * var(--ub));}
}
@media (max-width:430px){
  .vol .a{display:none;}
  #chapBody .tr-a{display:none;}
  .shelf-note{font-size:12px;}
  .window{width:min(124px,30%);}
}
@media (prefers-reduced-motion:reduce){
  *{transition-duration:.01ms !important;animation:none !important;}
  .book:hover .boards,.book:focus-visible .boards{transform:translateY(calc(-7 * var(--u))) rotate(var(--lz, 0deg)) rotateY(var(--ry)) rotateX(var(--tilt));}
  /* the boards stay at rest angles here, so the type's counter-rotation
     must stay at rest too or the pair stops cancelling */
  .book:hover .f-type,.book:focus-visible .f-type{
    transform:translate(-50%,-50%) translateZ(calc(var(--d) / 2 + 3px))
              rotateX(calc(-1 * var(--tilt))) rotateY(calc(-1 * var(--ry))) rotate(calc(-1 * var(--lz, 0deg)));
  }
  .book:hover,.book:focus-visible{outline:2px solid var(--cypress);outline-offset:5px;}
}

/* ═══ the cat ═════════════════════════════════════════════════ */
/* A marmalade tabby, because the room is lit for one: the sun side of
   the coat is the same warm the window throws, and the shaded fur
   drifts periwinkle like everything else in this room. Detail budget:
   stripes, ears, closed eyes, one paw line — anything finer turns to
   mush at 250px. To use your own picture instead, drop it in the repo
   and swap the <svg> for <img src="cat.png" alt=""> — .perch sizing holds. */
.cat-breathe{transform-box:view-box;transform-origin:106px 112px;animation:catBreathe 4.8s ease-in-out infinite;}
.cat-head{transform-box:view-box;transform-origin:65px 108px;animation:catBob 4.8s ease-in-out infinite;}
/* the flick occupies a sliver of a long timeline, so it reads as
   occasional rather than metronomic */
.cat-tail{transform-box:view-box;transform-origin:174px 93px;animation:catTail 17s linear infinite;}
@keyframes catBreathe{0%,100%{transform:scale(1,1)}42%{transform:scale(1.004,1.021)}80%{transform:scale(1,1)}}
@keyframes catBob{0%,100%{transform:translateY(0)}42%{transform:translateY(-.7px)}80%{transform:translateY(0)}}
@keyframes catTail{0%,7%,100%{transform:rotate(0)}10.5%{transform:rotate(-4.5deg)}15%{transform:rotate(1.6deg)}21%{transform:rotate(0)}
                   63%{transform:rotate(0)}66%{transform:rotate(-2.6deg)}72%{transform:rotate(0)}}
