/* Hallmark · redesign: full (the Cave's sections; brand, nav and footer kept) · shape: catalogue as one wall (four collections, one shelf system) · genre: editorial · theme: brand (the portfolio's bone ground, Plus Jakarta Sans, shared nav and footer) · hero: Banner · nav: the site pill · footer: the site statement footer · signature: the pine shelf wall, every object standing on it and picked up off it · sections: hero, books, last played, handhelds, handphone (all on planks), colophon, footer */
/* Hallmark · pre-emit critique: P4 H4 E4 S5 R5 V4 · weakest: P, a collection page argues little; the wall carries it by being one room instead of four bands */
/* Hallmark · waive 47b · phone silhouettes drawn in CSS, the fallback when a phone photo is missing · they are illustrations of real phones, labelled as such under the shelf */
/* Hallmark · dropped tabindex and role="region" on [data-phones] · the phones no longer scroll sideways; they stand on planks like everything else */

/* The Cave shares the portfolio's daylight ground and typography.
   Depth belongs to the collected objects; chrome stays quiet. */
.cave-page { --font-label: var(--font-screen); }
:root {
  --cave-focus: var(--ink);
  --cave-text-display: clamp(40px, 5.6vw, 76px);
  --cave-text-section: clamp(32px, 3.6vw, 52px);
  --cave-text-lead: clamp(17px, 1.5vw, 21px);
  --cave-section-gap: clamp(88px, 10vw, 144px);
  --cave-lamp-glow: rgb(255 196 120 / .15);
  --cave-sun: #f6ead0;
  /* the plank's engraved label */
  --cave-pine-ink: #55391a;
  --cave-pine-ink-lip: rgb(255 244 222 / .7);
  /* the shelf wall: an airy recess, light pine planks */
  --cave-pine-back: #d4b68a;
  --cave-pine-top: #ecd7b4;
  --cave-pine-face: #e2c69d;
  --cave-pine-face-low: #c7a274;
  --cave-pine-edge: #f7e9cf;
  --cave-pine-grain: rgb(140 100 52 / .07);
  --cave-wall-meet: rgb(96 70 40 / .3);
  --cave-cast-near: rgb(72 52 28 / .4);
  --cave-cast-far: rgb(72 52 28 / .24);
  --cave-contact: rgb(50 32 14 / .75);
  --cave-contact-clear: rgb(58 38 18 / 0);
  --cave-book-cast: rgb(64 48 30 / .26);
  --cave-umbra: rgb(52 36 16 / .62);
  --cave-penumbra: rgb(66 46 24 / .38);
  --cave-shadow-clear: rgb(66 46 24 / 0);

  /* material colours: wood, paper, cloth, plastic, glass and shade for the objects */
  --cave-veil: rgb(49 43 34 / .12);
  --cave-book-cover-1: #c9c2ad;
  --cave-book-cover-2: rgb(65 50 35 / .2);
  --cave-book-cover-3: rgb(255 255 255 / .17);
  --cave-book-cover-4: rgb(0 0 0 / .15);
  --cave-book-cover-5: rgb(0 0 0 / .12);
  --cave-book-cover-6: rgb(255 255 255 / .1);
  --cave-book-cover-7: rgb(0 0 0 / .1);
  --cave-book-cover-8: rgb(55 43 28 / .1);
  --cave-shelf-book-back-1: rgb(0 0 0 / .4);
  --cave-shelf-book-pages-1: #eee5d2;
  --cave-shelf-book-pages-2: #c9bda6;
  --cave-shelf-book-pages-3: rgb(75 61 42 / .35);
  --cave-shelf-book-spine-1: rgb(0 0 0 / .36);
  --cave-shelf-book-spine-2: rgb(255 255 255 / .14);
  --cave-shelf-book-spine-3: rgb(0 0 0 / .35);
  --cave-book-fallback-1: #f7efdf;
  --cave-object-caption-1: #f4f0e9;
  --cave-object-caption-2: rgb(75 62 45 / .12);
  --cave-jewel-case-1: #1a2224;
  --cave-jewel-case-2: rgb(31 38 38 / .18);
  --cave-jewel-case-3: rgb(31 38 38 / .16);
  --cave-jewel-case-4: rgb(240 245 240 / .32);
  --cave-jewel-case-5: rgb(24 33 34 / .72);
  --cave-jewel-case-6: rgb(0 0 0 / .28);
  --cave-case-interior-1: #222c2f;
  --cave-case-interior-2: rgb(206 221 214 / .18);
  --cave-case-interior-3: rgb(0 0 0 / .48);
  --cave-case-disc-tray-1: #152023;
  --cave-case-disc-tray-2: #607074;
  --cave-case-disc-tray-3: rgb(0 0 0 / .6);
  --cave-case-disc-1: #a8b2ad;
  --cave-case-disc-2: rgb(244 249 243 / .5);
  --cave-case-disc-3: rgb(0 0 0 / .25);
  --cave-case-disc-4: #1d1c1a;
  --cave-case-disc-5: rgb(214 222 212 / .7);
  --cave-case-disc-6: rgb(26 34 31 / .75);
  --cave-case-front-1: #20292b;
  /* the keep case's clear plastic and hinge metal (game-case.js #d0d3ca, #9ca59a) */
  --cave-case-plastic: #cfd3ca;
  --cave-case-plastic-lit: #e6e9e2;
  --cave-case-plastic-shade: #b7bcb2;
  --cave-case-plastic-edge: rgb(105 121 110 / .28);
  --cave-case-plastic-lip: rgb(255 255 255 / .7);
  --cave-case-hinge: #9ca59a;
  --cave-case-hinge-lit: #d3d8d0;
  --cave-case-hinge-shade: #6f786d;
  --cave-case-insert-back: #d2cbbb;
  --cave-case-front-2: rgb(0 0 0 / .42);
  --cave-case-cover-1: #3b6064;
  --cave-case-cover-2: rgb(255 245 224 / .12);
  --cave-case-cover-3: rgb(46 31 21 / .15);
  --cave-case-cover-4: rgb(255 248 235 / .05);
  --cave-case-cover-5: rgb(42 29 19 / .12);
  --cave-case-cover-6: rgb(255 250 236 / .2);
  --cave-case-cover-7: rgb(41 28 18 / .13);
  --cave-case-cover-8: rgb(41 28 18 / .16);
  --cave-phone-body-1: #777976;
  --cave-phone-body-2: rgb(255 255 255 / .65);
  --cave-phone-body-3: rgb(0 0 0 / .45);
  --cave-phone-body-4: rgb(49 43 34 / .23);
  --cave-phone-screen-1: #242728;
  --cave-phone-screen-2: rgb(241 235 219 / .28);
  --cave-phone-screen-3: rgb(28 46 47 / .22);
  --cave-phone-camera-1: #1c2123;
  --cave-phone-camera-2: #414c51;
  --cave-phone-speaker-1: #202525;
  --cave-phone-home-1: #747773;
  --cave-phone-home-2: #303536;
  --cave-phone-controls-1: #c5c3b9;
  --cave-phone-navigation-1: #aaaead;
  --cave-phone-navigation-2: #404b50;
  --cave-phone-keyboard-1: #303d42;
  --cave-phone-keyboard-2: rgb(255 255 255 / .45);
  --cave-phone-body-5: rgb(33 43 38 / .22);
  --cave-phone-body-6: rgb(255 255 255 / .18);
  --cave-item-backdrop-1: rgb(49 43 34 / .28);
  --cave-item-dialog-1: rgb(255 255 255 / .55);
  --cave-item-dialog-2: #dedbd4;
  --cave-item-dialog-4: #efede7;
  --cave-item-close-1: #ece8df;
  --cave-item-close-2: #dedad1;
  --cave-item-preview-1: rgb(0 0 0 / .32);
  --cave-bf-spine-1: rgb(255 246 233 / .88);
  --cave-bf-pages-1: #efe6d2;
  --cave-bf-pages-2: #cfc3ab;
  --cave-bf-pages-3: rgb(90 75 50 / .45);
  --cave-book3d-shadow-1: rgb(83 74 59 / .28);
  --cave-item-navigation-1: #eeeae1;
  --cave-item-backdrop-2: rgb(49 43 34 / .7);
  --cave-dialog-cast-near: rgb(50 44 34 / .16);
  --cave-dialog-cast-far: rgb(50 44 34 / .2);
  /* the game viewer's controls */
  --cave-control-edge: #b9b2a4;
  --cave-control-fill: #e8e3d9;
  --cave-control-fill-hover: #f0ece3;
  --cave-control-focus: #486c60;
}

/* The afternoon wall. The Cave's ground is limewash pulled from the site
   green, not the site's bone: theme.css resolves --bg from --day-bg on
   the root, so the override has to sit on the root too, and win over
   theme.css (which loads later) by specificity. The night end is the
   same wall with the light gone. The nav and footer read the same
   tokens, so they sit on the wall instead of in daylight elsewhere. */
html:has(body.cave-page) {
  --day-bg: #c3cdc4;        --night-bg: #111712;
  --day-bg-deep: #b4c0b7;   --night-bg-deep: #0c110d;
  --day-ink-muted: #434d46; --night-ink-muted: #b3bdb5;
  --day-ink-faint: #525c55; --night-ink-faint: #9aa69d;
  --nav-pill: color-mix(in oklab, #d3dbd4 calc(var(--lit) * 100%), #18201a);
  --nav-pill-edge: color-mix(in oklab, #b9c4bb calc(var(--lit) * 100%), #29332b);
  --nav-pill-shadow: 0 2px 3px rgb(34 48 38 / calc(.16 + .3 * (1 - var(--lit))));
  --nav-ink: color-mix(in oklab, #4c5750 calc(var(--ink-lit) * 100%), #aab6ad);
  --nav-active: color-mix(in oklab, #eef1ec calc(var(--lit) * 100%), #263028);
  --nav-active-edge: color-mix(in oklab, #c7d0c8 calc(var(--lit) * 100%), #36423a);
  --nav-veil: color-mix(in oklab, rgb(195 205 196 / .45) calc(var(--lit) * 100%), rgb(17 23 18 / .45));
  --footer-floor-1: color-mix(in oklab, #bcc7be calc(var(--lit) * 100%), #0f140f);
  --footer-floor-2: color-mix(in oklab, #b4bfb6 calc(var(--lit) * 100%), #0c110d);
  --footer-floor-3: color-mix(in oklab, #adb9b0 calc(var(--lit) * 100%), #090c09);
}
/* No graph paper on this wall. */
html:has(body.cave-page)::before { content: none; }

/* Hero, Banner posture: the headline across the page, the shelves start
   above the fold. The old 260px sidebar squeezed a 38px head into four
   lines; the collection is the picture, so the words step aside for it. */
.collection-layout { position: relative; padding-top: clamp(64px, 7vw, 120px); }
.collection-intro h1 { margin: 0; font-size: clamp(40px, 4.4vw, 64px); line-height: 1.02; font-weight: 600; letter-spacing: -.045em; overflow-wrap: break-word; min-width: 0; max-width: 13ch; text-wrap: balance; }

/* Window light. One window up and to the right: four panes split by the
   mullion, sheared so the patch falls down-left across wall and shelf.
   The wall layer sits behind the shelves; the second copy warms what
   stands in it (soft-light, so dark ink stays dark). It drifts down the
   wall as the page scrolls, the afternoon moving on. It is daylight, so
   it goes when the room light does; the lamp takes over at night. */
.cave-light { position: absolute; inset: 0; z-index: 0; overflow: clip; pointer-events: none; opacity: var(--lit); }
.cave-light--objects { z-index: 2; mix-blend-mode: soft-light; }
.cave-sun { position: absolute; top: -40px; right: max(-120px, calc(50% - 840px)); width: 900px; height: 1000px; filter: blur(16px); transform: skewX(-24deg); transform-origin: top right; }
.cave-sun i { position: absolute; width: 250px; background: var(--cave-sun); }
.cave-sun i:nth-child(1) { left: 300px; top: 60px; height: 380px; opacity: .6; }
.cave-sun i:nth-child(2) { left: 572px; top: 60px; height: 380px; opacity: .6; }
.cave-sun i:nth-child(3) { left: 300px; top: 462px; height: 420px; opacity: .46; }
.cave-sun i:nth-child(4) { left: 572px; top: 462px; height: 420px; opacity: .46; }
.cave-light--objects .cave-sun i { opacity: .85; }
@supports (animation-timeline: scroll()) {
  @media (prefers-reduced-motion: no-preference) {
    .cave-sun { animation: cave-sun-drift linear both; animation-timeline: scroll(root); }
    @keyframes cave-sun-drift { to { transform: translateY(var(--sun-travel, 320vh)) skewX(-17deg); } }
  }
}
@media (max-width: 600px) { .cave-sun { width: 560px; height: 700px; right: -180px; } .cave-sun i { width: 150px; } .cave-sun i:nth-child(1), .cave-sun i:nth-child(3) { left: 190px; } .cave-sun i:nth-child(2), .cave-sun i:nth-child(4) { left: 356px; } .cave-sun i:nth-child(1), .cave-sun i:nth-child(2) { height: 260px; } .cave-sun i:nth-child(3), .cave-sun i:nth-child(4) { top: 340px; height: 300px; } }

/* Lamp in the dark. With the room light off the words stay readable
   (theme.css carries them to night), and the collection falls into
   shadow. Each object's --lamp, set by lamp.js from its distance to the
   lamp, brings it back: full light under the beam, a warmer cast as it
   catches the bulb. In a lit room (1 - --lit) is 0 and none of this
   shows. The glow is the pool on the wall behind the shelves; .wrap
   (z 1) keeps the page above it. */
.cave-lamp { position: fixed; inset: 0; z-index: 0; pointer-events: none; opacity: calc(1 - var(--lit)); background: radial-gradient(circle 460px at var(--lamp-x, 50%) var(--lamp-y, 45%), var(--cave-lamp-glow), transparent 72%); }
.cave-dark .collection-content .collectible { --lamp-floor: .84; filter: brightness(calc(1 - (1 - var(--lit)) * (1 - var(--lamp, 0)) * var(--lamp-floor))) sepia(calc((1 - var(--lit)) * var(--lamp, 0) * .18)); }

/* Printed things keep their own ink: the dialog card and the shelf
   captions are light surfaces in any room, so the night palette must
   not reach the type on them (the same pin theme.css applies to paper
   artifacts elsewhere on the site). */

/* Sections: each shelf is one composition, set big enough to be the
   reason for its screen. Section heads were 13px under the hero; now
   they sit a clear step below the display. */
/* One wall, one rhythm: the gap between two collections is the gap
   between two planks, so the page reads as shelves going down a wall,
   not as four headed sections. Each collection is named on its first
   plank (see .shelf-label), so there is no heading line above it. */
.collection-section { scroll-margin-top: 120px; padding-block: clamp(64px, 7vw, 96px) 0; }
.collection-section__heading { margin-bottom: 40px; }
.collection-section__heading:not(:has(h2, p)) { display: none; }
/* Measured at 1440x900 and 390x844: the first screen ends below a
   whole plank, never through the covers. */
.collection-section--books { padding-top: clamp(48px, 6vw, 100px); }
/* On a desktop screen the first plank rides low enough that the second
   row starts under the fold; the window light fills the wall above it. */
@media (min-width: 961px) and (min-height: 700px) { .collection-section--books { padding-top: clamp(80px, calc(100svh - min(7vw, 120px) - 540px), 480px); } }

/* One wall. Every collection hangs on the same bone wall as the books, on
   the same pine planks, so the page reads as one room you walk along, not
   four bands of different paper. Only the objects change. */
.collection-section--phones .collection-section__heading { display: grid; gap: 12px; }
.collection-colophon { padding-block: var(--cave-section-gap) 0; }
/* Floating shelves, straight on the room's wall. Each row stands on a
   pine plank with real volume: a top surface seen in slight perspective
   (narrower at the wall, lighter where it faces the light) and a thick
   front edge with a lit arris. Its mounts are hidden in the wall, the way
   a floating shelf is hung, so nothing dangles under it. Two shadows put
   it on the wall, a tight one under the front edge and a wide soft one
   below, and each object leaves a contact shadow where it meets the
   plank. In a dark room the plank dims with the room; the lamp lights
   what stands on it. */
.shelf-niche { position: relative; isolation: isolate; padding-top: clamp(8px, 2vw, 24px); }
.shelf-niche .collection-section__heading { margin-bottom: clamp(28px, 4vw, 48px); }
.book-shelves, .game-grid, .device-grid, .phone-grid { display: grid; gap: clamp(64px, 7vw, 96px); padding: 0 clamp(16px, 3vw, 40px); }
.shelf-row { --plank-top: 18px; --plank-front: 22px; --plank-over: 20px; position: relative; display: flex; justify-content: space-evenly; align-items: flex-end; gap: 16px; padding: 0 8px calc(var(--plank-front) + var(--plank-top) * .55); }
/* The collection's name, cut into the front edge of its first plank,
   with the count at the other end. It is the section's real h2, kept
   out of the aria-hidden plank so it stays a heading. */
.shelf-label { position: absolute; z-index: 1; left: calc(-1 * var(--plank-over) + 24px); right: calc(-1 * var(--plank-over) + 24px); bottom: 0; height: var(--plank-front); display: flex; align-items: center; justify-content: space-between; gap: 16px; pointer-events: none; color: color-mix(in oklab, var(--cave-pine-ink) calc(var(--lit) * 100%), #c9c0ae); text-shadow: 0 1px 0 color-mix(in oklab, var(--cave-pine-ink-lip) calc(var(--lit) * 100%), transparent); }
.shelf-label h2 { margin: 0; font-size: 13px; line-height: 1; font-weight: 600; letter-spacing: .01em; }
.shelf-label span { font-size: 13px; line-height: 1; font-weight: 500; font-variant-numeric: tabular-nums; }
.shelf-plank { position: absolute; left: calc(-1 * var(--plank-over)); right: calc(-1 * var(--plank-over)); bottom: 0; height: calc(var(--plank-top) + var(--plank-front)); pointer-events: none; filter: brightness(calc(.34 + .66 * var(--lit))); }
.shelf-plank__top { position: absolute; left: 0; right: 0; top: 0; height: var(--plank-top); clip-path: polygon(1.6% 0, 98.4% 0, 100% 100%, 0 100%); background: repeating-linear-gradient(90deg, var(--cave-pine-grain) 0 2px, transparent 2px 11px), linear-gradient(var(--cave-pine-back), var(--cave-pine-top)); box-shadow: inset 0 3px 4px -2px var(--cave-wall-meet); }
.shelf-plank__front { position: absolute; left: 0; right: 0; bottom: 0; height: var(--plank-front); border-radius: 0 0 3px 3px; background: repeating-linear-gradient(0deg, var(--cave-pine-grain) 0 1px, transparent 1px 5px), linear-gradient(var(--cave-pine-face), var(--cave-pine-face-low)); box-shadow: inset 0 1px 0 var(--cave-pine-edge), inset 0 -2px 3px -1px var(--cave-cast-far); }
/* The shadow the plank throws on the wall. Light comes from above and a
   little in front, so it falls straight down: a tight, dark umbra right
   under the front edge where the plank nearly touches the wall, and a
   wide penumbra that softens and fades the further it travels. */
.shelf-plank__shadow { position: absolute; z-index: -2; top: 100%; left: 0; right: 0; height: 120px; pointer-events: none; }
.shelf-plank__shadow::before { content: ''; position: absolute; left: 1.5%; right: 1.5%; top: -3px; height: 22px; border-radius: 0 0 50% 50% / 0 0 100% 100%; background: linear-gradient(var(--cave-umbra), var(--cave-shadow-clear)); filter: blur(3px); }
.shelf-plank__shadow::after { content: ''; position: absolute; left: -3%; right: -3%; top: -4px; height: 100%; background: radial-gradient(ellipse 52% 100% at 50% 0, var(--cave-penumbra), var(--cave-shadow-clear) 78%); filter: blur(10px); }
.book-row .book { --shelf-book-depth: 22px; position: relative; z-index: 1; flex: none; width: clamp(92px, 12.5vw, 156px); }
/* Face out, like everything else on the wall: the cover square to the
   room. The page block is still there, and shows when a hover turns it. */
.book-row .object-motion { --rest-ry: 0deg; transform: rotateY(var(--rest-ry)); }
/* When a book is lifted (hover, focus) its contact shadow spreads and
   fades, the way a shadow loosens as an object leaves the surface; a
   press settles it back to a tight, dark mark. */
.shelf-row .collectible:hover::before, .shelf-row .collectible:focus-visible::before { transform: scale(1.3, 1.6); opacity: .4; }
.shelf-row .collectible:active::before { transform: scale(1.05, 1.15); opacity: .8; transition-duration: 120ms; }
.shelf-row .collectible::before { transition: transform 320ms var(--ease-out), opacity 320ms var(--ease-out); content: ''; position: absolute; z-index: -1; left: 0; right: 0; bottom: -11px; margin-inline: auto; width: var(--contact-w, 80%); height: 16px; border-radius: 50%; background: radial-gradient(closest-side, var(--cave-contact), var(--cave-contact-clear)); pointer-events: none; }
.book-row .book { --contact-w: calc(min(100%, 148% * var(--book-height, 1) * var(--cover-ar, .6757)) + 22px); }
/* A handheld's shadow is its own footprint on the plank; the WebGL frame
   is wider than the machine, so the shadow is narrower than the frame. */
.device-row .device { --contact-w: 72%; }

/* Last played: the cases stand face out on their planks, the way a game
   shelf is kept, a touch smaller than the books beside them. */
.game-row .game { flex: none; width: clamp(88px, 12vw, 150px); --contact-w: calc(100% + 16px); }

/* Handhelds: three machines on one plank, each the reason to look. */
.device-row { gap: clamp(8px, 2vw, 32px); }
.device-row .device { flex: 1 1 0; min-width: 0; max-width: 340px; }
.device-row .device-visual { height: clamp(150px, 22vw, 290px); }

/* Handphone: twelve phones in the order they were owned, face out on two
   planks like the books. Each stands at its real height against the
   tallest (--phone-scale, from the catalog), so the row is a true skyline
   the way the books' uneven heights are; width follows the photo. */
.phone-row { --phone-shelf-h: clamp(140px, 15vw, 210px); }
.phone-row .phone { flex: none; width: auto; --contact-w: 96%; }
/* A phone standing on its edge touches the plank along a thin line, not
   a footprint: its shadow is a tight sliver tucked right under the base,
   never an oval spilling out in front of it. */
.phone-row .phone::before { bottom: -3px; height: 6px; opacity: .45; }
.phone-row .phone:hover::before, .phone-row .phone:focus-visible::before { transform: scale(1.1, 1.4); opacity: .25; }
.phone-row .phone:active::before { transform: none; opacity: .5; }
.phone-row .phone-visual { height: calc(var(--phone-shelf-h) * var(--phone-scale, 1)); }
.phone-row .phone-image { width: auto; height: 100%; }
.phone-row .phone-body { transform: none; }

/* Every object on the wall names itself the same way: the small caption
   above it on hover or focus, and in full when picked up. Nothing is
   written under the shelves. */
.shelf-row .device-name { display: none; }
.shelf-row .collectible { z-index: 1; }
.collectible { position: relative; min-width: 0; border: 0; background: none; padding: 0; color: inherit; font: inherit; cursor: pointer; text-align: left; -webkit-tap-highlight-color: transparent; touch-action: manipulation; transition: opacity 120ms linear; }
.collectible:focus-visible { outline: 2px solid var(--cave-focus); outline-offset: 8px; border-radius: 3px; }
.collectible.is-in-flight { pointer-events: none; }
/* The shelf label is part of the shelf, not the object being carried.
   Keep it stable while the physical object is in transit so navigating
   or interrupting a flight never leaves a fading or missing caption. */
/* The shelf object and its flying copy trade places in one frame, so
   there is never a moment with two half-visible objects. No fade here. */
.collectible.is-in-flight .object-motion,
.collectible.is-in-flight .device-visual { opacity: 0; }
.object-motion { display: block; position: relative; transform-style: preserve-3d; will-change: auto; }
.book { width: 100%; aspect-ratio: 1 / 1.48; transform-origin: 50% 90%; perspective: 800px; }
/* The cuboid fits its own art: width is height × the cover file's ratio,
   bottom-anchored and centered in the column, so the cover fills it
   exactly with no crop and no distortion. Heights still vary per book —
   that uneven skyline is the whole look — but narrow books now read
   narrow instead of sliced. Capped at full width: a ratio wider than the
   bay allows means the catalog height wants lowering, not a spill. */
.book .object-motion { height: calc(100% * var(--book-height, 1)); position: absolute; bottom: 0; left: 0; right: 0; margin: 0 auto; width: min(100%, calc(148% * var(--book-height, 1) * var(--cover-ar, .6757))); }
.book .object-motion { transform-style: preserve-3d; }
.book-cover { position: absolute; inset: 0; border-radius: 1px 3px 3px 1px; background: var(--cloth); box-shadow: 7px 5px 12px var(--cave-book-cast); overflow: hidden; transform: translateZ(calc(var(--shelf-book-depth, 9px) / 2)); backface-visibility: hidden; }
.book-cover img { width: 100%; height: 100%; object-fit: cover; display: block; }
.book-cover::after { content: ''; position: absolute; inset: 0; background: linear-gradient(90deg, var(--cave-book-cover-3), var(--cave-book-cover-4) 3%, transparent 7%, var(--cave-book-cover-5) 9%, var(--cave-book-cover-6) 11%, transparent 16%, transparent 95%, var(--cave-book-cover-7)); box-shadow: inset 0 0 0 1px var(--cave-book-cover-8); }
.shelf-book-back, .shelf-book-pages, .shelf-book-spine { position: absolute; transform-style: preserve-3d; pointer-events: none; }
.shelf-book-back { inset: 0; background: linear-gradient(var(--cave-shelf-book-back-1), var(--cave-shelf-book-back-1)), var(--cloth); transform: rotateY(180deg) translateZ(calc(var(--shelf-book-depth, 9px) / 2)); }
.shelf-book-pages { top: 3px; bottom: 3px; right: calc(var(--shelf-book-depth, 9px) / -2); width: var(--shelf-book-depth, 9px); transform: rotateY(90deg); background: repeating-linear-gradient(0deg, var(--cave-shelf-book-pages-1) 0 2px, var(--cave-shelf-book-pages-2) 2px 3px); box-shadow: inset 0 0 4px var(--cave-shelf-book-pages-3); }
.shelf-book-spine { top: 0; bottom: 0; left: calc(var(--shelf-book-depth, 9px) / -2); width: var(--shelf-book-depth, 9px); transform: rotateY(-90deg); background: linear-gradient(90deg, var(--cave-shelf-book-spine-1), transparent 42%, var(--cave-shelf-book-spine-2), transparent 70%, var(--cave-shelf-book-spine-3)), var(--cloth); }
.book-fallback { display: grid; place-content: center; height: 100%; padding: 14px; color: var(--cave-book-fallback-1); font-size: 12px; text-align: center; }
.object-caption { position: absolute; left: 50%; bottom: calc(100% + 17px); width: max-content; max-width: 220px; transform: translate(var(--caption-x, -50%), 4px); padding: 9px 12px; border-radius: 8px; background: var(--cave-object-caption-1); box-shadow: 0 4px 20px var(--cave-object-caption-2); font-size: 11px; line-height: 1.5; text-align: center; pointer-events: none; transition: opacity 160ms var(--ease-out), transform 200ms var(--ease-out); z-index: 3; }
/* Hidden at rest only once script runs: captions are built by script, and a hover or focus shows them. */
.js .object-caption { opacity: 0; }
.collectible:hover, .collectible:focus-visible { z-index: 4; }
.collectible:hover .object-caption, .collectible:focus-visible .object-caption { opacity: 1; transform: translate(var(--caption-x, -50%), 0); }
.game { aspect-ratio: .7105; perspective: 900px; }
.game .object-motion { width: 100%; height: 100%; }
/* The same clear keep case the detail view renders in 3D (game-case.js:
   1.35 x 1.9, insert inset .09, spine slab .075, two hinge knuckles .2 in
   from each end), drawn flat so the case on the shelf and the case in your
   hand are one object. Proportions are in percent of that geometry. */
.jewel-case { position: relative; display: block; width: 100%; height: 100%; transform-style: preserve-3d; border-radius: 2.6% / 1.85%; isolation: isolate; background: var(--cave-jewel-case-1); box-shadow: 1px 2px 2px var(--cave-jewel-case-2), 4px 9px 14px var(--cave-jewel-case-3); }
/* the spine slab along the hinge edge */
.jewel-case::before { content: ''; position: absolute; z-index: 4; left: -2.2%; width: 5.6%; top: 2.1%; bottom: 2.1%; border-radius: 3px; background: linear-gradient(90deg, var(--cave-case-plastic-shade), var(--cave-case-plastic-lit) 55%, var(--cave-case-plastic)); box-shadow: inset 0 0 0 1px var(--cave-case-plastic-edge); }
/* the two hinge knuckles */
.jewel-case::after { content: ''; position: absolute; z-index: 5; left: -1.4%; width: 4.1%; top: 6.55%; bottom: 6.55%; border-radius: 2px; background: linear-gradient(90deg, var(--cave-case-hinge-shade), var(--cave-case-hinge-lit) 45%, var(--cave-case-hinge)); -webkit-mask-image: linear-gradient(to bottom, #000 0 9.1%, transparent 9.1% 90.9%, #000 90.9%); mask-image: linear-gradient(to bottom, #000 0 9.1%, transparent 9.1% 90.9%, #000 90.9%); }
.case-interior { position: absolute; inset: 0 0 0 3px; display: grid; place-items: center; overflow: hidden; border-radius: 0 1px 1px 0; background: var(--cave-case-interior-1); box-shadow: inset 0 0 0 1px var(--cave-case-interior-2), inset 0 2px 7px var(--cave-case-interior-3); }
.case-disc-tray { display: grid; place-items: center; width: 82%; aspect-ratio: 1; border-radius: 50%; background: var(--cave-case-disc-tray-1); box-shadow: inset 0 0 0 2px var(--cave-case-disc-tray-2), inset 0 2px 6px var(--cave-case-disc-tray-3); }
.case-disc { position: relative; display: block; width: 88%; aspect-ratio: 1; border-radius: 50%; background: var(--disc-art) center / cover, var(--cave-case-disc-1); box-shadow: 0 0 0 1px var(--cave-case-disc-2), 0 2px 5px var(--cave-case-disc-3); mask-image: radial-gradient(circle, transparent 0 6%, var(--cave-case-disc-4) 6.8%); }
.case-disc::after { content: ''; position: absolute; inset: 39%; border-radius: 50%; border: 4px solid var(--cave-case-disc-5); box-shadow: 0 0 0 1px var(--cave-case-disc-6), inset 0 0 0 1px var(--cave-case-disc-6); }
/* The lid: a pane of clear plastic with the printed insert under it, a
   lit top lip and a faint self-coloured edge, exactly as the 3D case. */
.case-front { position: absolute; z-index: 3; inset: 0; transform-origin: left center; transform-style: preserve-3d; border-radius: 2.6% / 1.85%; background: linear-gradient(160deg, var(--cave-case-plastic-lit), var(--cave-case-plastic) 38%, var(--cave-case-plastic-shade)); box-shadow: inset 0 0 0 1px var(--cave-case-plastic-edge), inset 0 1px 0 var(--cave-case-plastic-lip); }
.case-front::before { content: ''; position: absolute; z-index: 2; inset: 0; border-radius: inherit; pointer-events: none; background: linear-gradient(115deg, rgb(255 255 255 / .2), transparent 26%, transparent 70%, rgb(255 255 255 / .08)); }
.case-cover { position: absolute; inset: 4.74% 6.67%; display: block; overflow: hidden; background: var(--case-art) center / cover, var(--cave-case-cover-1); filter: saturate(.82) brightness(.92) contrast(.96); transform: translateZ(.5px); backface-visibility: hidden; -webkit-backface-visibility: hidden; }
/* A muted raking-light layer keeps game artwork in the same physical
   lighting family as the book covers instead of emitting poster colour. */
.case-cover::after { content: ''; position: absolute; inset: 0; pointer-events: none; background: linear-gradient(100deg, var(--cave-case-cover-2), transparent 20%, transparent 72%, var(--cave-case-cover-3)), linear-gradient(180deg, var(--cave-case-cover-4), transparent 32%, var(--cave-case-cover-5)); box-shadow: inset 1px 0 var(--cave-case-cover-6), inset -1px 0 var(--cave-case-cover-7), inset 0 -1px var(--cave-case-cover-8); }
/* Open the lid and you see the back of the printed insert, plain matte
   paper, the way the 3D case shows it; never the cover in mirror image. */
.case-sleeve { display: block; position: absolute; inset: 4.74% 6.67%; background: var(--cave-case-insert-back); box-shadow: inset 0 0 0 1px var(--cave-case-plastic-edge); transform: rotateY(180deg) translateZ(.5px); backface-visibility: hidden; -webkit-backface-visibility: hidden; }
.device { width: 100%; }
.device-visual { display: block; height: 250px; position: relative; }
.device-visual canvas { display: block; width: 100%; height: 100%; }
.device-name { display: block; font-size: 14px; font-weight: 600; text-align: center; margin-top: 12px; }
.device-loading { position: absolute; inset: 0; display: grid; place-items: center; color: var(--ink-muted); font-size: 12px; text-align: center; }

/* Front silhouettes keep the phone history tangible without loading twelve
   more WebGL scenes. Shadows and highlights describe the hardware edges. */
.phone { width: 100%; }
.phone .object-motion { display: block; }
.phone-visual { display: grid; place-items: end center; height: clamp(130px, 15vw, 210px); position: relative; }
.phone-image { display: block; width: 100%; height: 100%; min-height: 0; object-fit: contain; object-position: 50% 100%; }
.phone-image + .phone-body { display: none; }
.phone-body { display: block; width: 92px; height: 184px; position: relative; border-radius: 17px; background: var(--phone-finish); border: 2px solid var(--cave-phone-body-1); box-shadow: inset 2px 0 2px var(--cave-phone-body-2), inset -3px 0 3px var(--cave-phone-body-3), 4px 7px 8px var(--cave-phone-body-4); transform: rotate(-5deg); }
.phone-screen { position: absolute; inset: 5px; border: 3px solid var(--cave-phone-screen-1); border-radius: 12px; background: var(--phone-screen); overflow: hidden; }
.phone-screen::before { content: ''; position: absolute; width: 160%; height: 95%; left: -55%; top: 35%; border-radius: 50%; background: var(--cave-phone-screen-2); transform: rotate(-35deg); }
.phone-screen::after { content: ''; position: absolute; width: 110%; height: 90%; left: 35%; top: 45%; border-radius: 50%; background: var(--cave-phone-screen-3); transform: rotate(-35deg); }
.phone-camera { position: absolute; width: 6px; height: 6px; top: 10px; left: calc(50% - 3px); background: var(--cave-phone-camera-1); border: 1px solid var(--cave-phone-camera-2); border-radius: 50%; }
.phone-speaker { position: absolute; width: 22px; height: 3px; top: 9px; left: calc(50% - 11px); background: var(--cave-phone-speaker-1); border-radius: 3px; }
.phone-body--punch .phone-speaker, .phone-body--punch-right .phone-speaker, .phone-body--fold .phone-speaker { top: 3px; height: 2px; }
.phone-body--punch-right .phone-camera { left: auto; right: 13px; top: 13px; }
.phone-body--notch .phone-camera, .phone-body--notch-small .phone-camera { left: calc(50% + 15px); top: 11px; z-index: 1; }
.phone-body--notch .phone-speaker, .phone-body--notch-small .phone-speaker { width: 48px; height: 13px; top: 5px; left: calc(50% - 24px); border-radius: 0 0 9px 9px; background: var(--cave-phone-screen-1); }
.phone-body--notch-small .phone-speaker { width: 36px; left: calc(50% - 18px); }
.phone-body--notch-small .phone-camera { left: calc(50% + 9px); }
.phone-body--home { width: 86px; height: 158px; border-radius: 9px; }
.phone-body--home .phone-screen, .phone-body--classic .phone-screen { inset: 23px 6px 25px; border-radius: 2px; border-width: 2px; }
.phone-body--home .phone-camera, .phone-body--classic .phone-camera, .phone-body--capacitive .phone-camera, .phone-body--speaker .phone-camera { top: 9px; left: 17px; width: 4px; height: 4px; }
.phone-home { position: absolute; bottom: 6px; left: calc(50% - 8px); width: 16px; height: 16px; border: 1px solid var(--cave-phone-home-1); border-radius: 50%; background: var(--cave-phone-home-2); }
.phone-body--classic { border-radius: 10px; }
.phone-body--classic .phone-home { width: 23px; height: 8px; left: calc(50% - 11.5px); bottom: 10px; border-radius: 4px; }
.phone-body--capacitive .phone-screen { inset: 20px 5px; border-radius: 3px; }
.phone-controls { position: absolute; bottom: 2px; width: 100%; font-size: 12px; color: var(--cave-phone-controls-1); word-spacing: 6px; text-align: center; }
.phone-body--speaker .phone-screen { inset: 18px 5px; border-radius: 5px; }
.phone-body--speaker .phone-speaker { width: 32px; left: calc(50% - 16px); }
.phone-body--keyboard { width: 100px; height: 160px; border-radius: 16px; }
.phone-body--keyboard .phone-screen { inset: 19px 8px 71px; border-radius: 3px; border-width: 2px; }
.phone-body--keyboard .phone-camera { display: none; }
.phone-navigation { position: absolute; top: 92px; left: calc(50% - 10px); width: 20px; height: 15px; border: 2px solid var(--cave-phone-navigation-1); border-radius: 4px; background: var(--cave-phone-navigation-2); }
.phone-keyboard { position: absolute; left: 7px; right: 7px; bottom: 9px; display: grid; grid-template-columns: repeat(10, 1fr); gap: 3px 2px; }
.phone-keyboard span { height: 9px; border-radius: 2px 2px 4px 4px; background: var(--cave-phone-keyboard-1); box-shadow: inset 0 1px 1px var(--cave-phone-keyboard-2); }
.phone-body--fold { width: 142px; max-width: calc(100% - 16px); height: 175px; border-radius: 11px; }
.phone-body--fold .phone-screen { border-radius: 7px; }
.phone-body--fold::after { content: ''; position: absolute; top: 6px; bottom: 6px; left: 50%; width: 2px; background: var(--cave-phone-body-5); box-shadow: 1px 0 2px var(--cave-phone-body-6); }
.phone-illustration-note { margin: 0; color: var(--ink-muted); font-size: 14px; line-height: 1.5; }
.phone-history { line-height: 1.8; }
.item-preview .phone { width: min(100%, 280px); cursor: default; touch-action: auto; }
.item-preview .phone-visual { height: 300px; }
.item-preview .phone-body { transform: rotate(-5deg) scale(1.4); }
@media (max-width: 600px) {
  .phone-row { --phone-shelf-h: 160px; }
  .item-preview .phone-visual { height: 240px; }
  .item-preview .phone-body { transform: rotate(-5deg) scale(1.15); }
}
.collection-credit { margin: 0; font-size: 14px; line-height: 1.6; color: var(--ink-muted); max-width: 65ch; }
.collection-credit a, .item-copy a { color: inherit; text-underline-offset: 3px; }
.collection-loading { font-size: 13px; color: var(--ink-muted); grid-column: 1 / -1; }
.item-overlay[hidden] { display: none; }
/* Keep the background materially behind the returning object. Clearing the
   blur before the 520ms flight completed made the page text pop back in
   halfway through the close. It now resolves during the final landing. */
/* The flying copy sits at its big (detail) size and is scaled down to the
   shelf with a transform, so it stays sharp and never animates layout. */
.item-flight-object { position: fixed; z-index: 1002; margin: 0; transform-origin: 0 0; transform-style: preserve-3d; pointer-events: none; perspective: 1000px; will-change: transform; }
.item-flight-object > * { width: 100%; height: 100%; }
.item-flight-object .flight-layer { position: absolute; inset: 0; }
/* Handheld flights travel as a transparent snapshot (a WebGL canvas can't
   be cloned): contain-fit, so differing frame aspects letterbox instead
   of distorting. */
.item-flight-object img { width: 100%; height: 100%; object-fit: contain; display: block; }
.item-flight-object .object-motion { width: 100%; height: 100%; transform-style: preserve-3d; }
.item-flight-object .device-visual { width: 100%; height: 100%; }
.item-preview .book { width: 200px; pointer-events: none; transform: rotateY(-14deg) rotateZ(-3deg); }
.item-preview .book .object-motion { height: 100%; }
.item-preview .game { width: 220px; height: auto; cursor: pointer; }
.item-preview .game .case-front { transition: transform 550ms var(--ease-out), box-shadow 550ms var(--ease-out); }
.item-preview .game.is-open .case-front { transform: rotateY(-108deg); box-shadow: 2px 0 9px var(--cave-item-preview-1); }
.item-preview .device { width: min(100%, 340px); justify-self: center; cursor: grab; touch-action: none; }
.item-preview .device-visual { height: 260px; }
.item-preview .is-flight-target { opacity: 0; pointer-events: none; transition: none !important; }
.item-preview .object-caption, .item-preview .device-name { display: none; }

/* Only the detail viewer gets a renderer. The shelf keeps its CSS cases. */
.game-stage { width: 100%; height: 340px; position: relative; min-width: 0; }
.game-renderer { position: absolute; inset: 0; opacity: 0; transition: opacity 150ms linear; pointer-events: none; }
.game-renderer.is-ready { opacity: 1; pointer-events: auto; }
.game-renderer canvas { display: block; width: 100%; height: 100%; touch-action: none; cursor: grab; border-radius: 4px; }
.game-renderer canvas:active { cursor: grabbing; }
/* Sized to where the 3D case comes to rest (measured: 233 x 320, a hair
   left of centre), so the case lands where the live one will stand. */
.item-preview .game-fallback { position: absolute; width: 233px; height: 320px; left: 50%; top: 50%; transform: translate(calc(-50% - 3px), -50%); transition: opacity 0s 240ms, visibility 0s 240ms; }
/* The printed case stays under the live one until the live one has fully
   faded in on top of it, then leaves: no blank frame at the hand-off. */
.game-fallback.is-replaced { opacity: 0; pointer-events: none; visibility: hidden; }
.game-renderer canvas:focus-visible, .game-controls button:focus-visible { outline: 2px solid var(--cave-control-focus); outline-offset: 3px; }
@media (max-width: 960px) { .game-stage { height: 290px; } .item-preview .game-fallback { width: 175px; height: 246px; } }
@media (max-height: 720px) and (min-width: 961px) { .game-stage { height: 280px; } }
/* The dialog book is a real cuboid you can turn. Geometry runs off two
   variables: --bw/--bh fix the display size, an inline --bt per book
   carries its catalog thickness. Faces are centered, then placed by the
   standard box recipe (front/back ±Z, spine/pages ±X, top/bottom ±Y). */
.book3d { --bh: 296px; --bw: calc(var(--bh) * var(--bar, .6757)); width: var(--bw); height: var(--bh); }
.book3d-scene { width: 100%; height: 100%; position: relative; perspective: 1200px; cursor: grab; touch-action: none; user-select: none; -webkit-user-select: none; }
.book3d-scene:active { cursor: grabbing; }
.book3d-scene:focus-visible { outline: 2px solid var(--cave-focus); outline-offset: 10px; border-radius: 4px; }
.book3d-obj { position: absolute; inset: 0; transform-style: preserve-3d; will-change: transform; }
.book3d-face { position: absolute; left: 50%; top: 50%; }
.bf-front { width: var(--bw); height: var(--bh); margin: calc(var(--bh) / -2) 0 0 calc(var(--bw) / -2); transform: translateZ(calc(var(--bt) / 2)); border-radius: 1px 3px 3px 1px; overflow: hidden; background: var(--cloth); }
.bf-front img { width: 100%; height: 100%; object-fit: cover; display: block; -webkit-user-drag: none; }
.bf-front::after { content: ''; position: absolute; inset: 0; background: linear-gradient(90deg, var(--cave-book-cover-3), var(--cave-book-cover-4) 3%, transparent 7%, var(--cave-book-cover-5) 9%, var(--cave-book-cover-6) 11%, transparent 16%, transparent 95%, var(--cave-book-cover-7)); box-shadow: inset 0 0 0 1px var(--cave-book-cover-8); }
.bf-back { width: var(--bw); height: var(--bh); margin: calc(var(--bh) / -2) 0 0 calc(var(--bw) / -2); transform: rotateY(180deg) translateZ(calc(var(--bt) / 2)); border-radius: 3px 1px 1px 3px; background: linear-gradient(var(--cave-phone-body-3), var(--cave-phone-body-3)), var(--cloth); }
.bf-spine { width: var(--bt); height: var(--bh); margin: calc(var(--bh) / -2) 0 0 calc(var(--bt) / -2); transform: rotateY(-90deg) translateZ(calc(var(--bw) / 2)); background: linear-gradient(90deg, var(--cave-shelf-book-back-1), transparent 28%, var(--cave-shelf-book-spine-2) 50%, transparent 72%, var(--cave-shelf-book-back-1)), var(--cloth); display: grid; place-items: center; overflow: hidden; }
.bf-spine span { writing-mode: vertical-rl; transform: rotate(180deg); font: 600 11px var(--font-screen); letter-spacing: 0; color: var(--cave-bf-spine-1); text-shadow: 0 1px 1px var(--cave-shelf-book-back-1); white-space: nowrap; max-height: 92%; overflow: hidden; }
.bf-pages { width: var(--bt); height: calc(var(--bh) - 6px); margin: calc((var(--bh) - 6px) / -2) 0 0 calc(var(--bt) / -2); transform: rotateY(90deg) translateZ(calc(var(--bw) / 2)); background: repeating-linear-gradient(0deg, var(--cave-bf-pages-1) 0 2px, var(--cave-bf-pages-2) 2px 3px); box-shadow: inset 0 0 6px var(--cave-bf-pages-3); }
.bf-top { width: var(--bw); height: var(--bt); margin: calc(var(--bt) / -2) 0 0 calc(var(--bw) / -2); transform: rotateX(90deg) translateZ(calc(var(--bh) / 2)); background: repeating-linear-gradient(90deg, var(--cave-bf-pages-1) 0 2px, var(--cave-bf-pages-2) 2px 3px); box-shadow: inset 0 0 6px var(--cave-bf-pages-3); }
.bf-bottom { width: var(--bw); height: var(--bt); margin: calc(var(--bt) / -2) 0 0 calc(var(--bw) / -2); transform: rotateX(-90deg) translateZ(calc(var(--bh) / 2)); background: repeating-linear-gradient(90deg, var(--cave-bf-pages-1) 0 2px, var(--cave-bf-pages-2) 2px 3px); box-shadow: inset 0 0 6px var(--cave-bf-pages-3); }
.book3d-shadow { position: absolute; left: 6%; right: 6%; bottom: -30px; height: 24px; border-radius: 50%; background: var(--cave-book3d-shadow-1); filter: blur(10px); }
/* The verdict, Goose-style: five stars, no halves, no decimals. Filled in
   ink, empty in faint — a quiet row, not a trophy. Hidden entirely for
   unplayed games (no stars without hours). */
@media (hover: hover) {
  /* The neighbours step back. Each dims whole: opacity on a book's 3D body
     would flatten it and paint the back cover over the front. */
  .shelf-row:has(.collectible:hover) .collectible:not(:hover) { opacity: .65; }
  .collectible { transition: opacity 120ms linear; }
}
@media (max-width: 960px) {
  .collection-section { scroll-margin-top: 150px; }
  .collection-section__heading { margin-bottom: 32px; }
  .shelf-row { --plank-top: 14px; --plank-front: 18px; --plank-over: 12px; gap: 8px; padding-inline: 4px; }
  .book-row .book { --shelf-book-depth: 12px; width: 24vw; }
  .game-row .game { width: 21vw; }
  .device-row .device-visual { height: clamp(130px, 26vw, 220px); }
  .item-preview .book { width: 145px; }
  .item-preview .book3d { --bh: 222px; }
  .item-preview .game { width: 175px; height: auto; }
  .item-preview .device-visual { height: 210px; }
  .object-caption { display: none; }
}
@media (prefers-reduced-motion: reduce) {
  .cave-page *, .cave-page *::before, .cave-page *::after { scroll-behavior: auto !important; transition: none !important; }
  .object-motion { transform: none !important; }
  .item-preview .book { transform: none; }
}
@media (prefers-contrast: more) { .collection-credit, .item-meta, .item-credit, .item-stars .off { color: var(--ink); } }

@media (hover: hover) and (prefers-reduced-motion: no-preference) {
  .game-row .case-front { transition: transform 300ms var(--ease-out); }
  .game-row .game:hover .case-front, .game-row .game:focus-visible .case-front { transform: rotateY(-5deg) translateZ(4px); }
}
@media (max-width: 600px) {
  .game-row .game { width: 26vw; }
}

/* The end objects of a plank keep their caption inside the wall. */
.shelf-row .collectible:last-of-type .object-caption { left: auto; right: 0; --caption-x: 0%; }
.shelf-row .collectible:first-child .object-caption { left: 0; --caption-x: 0%; }

/* The shelf captions are printed on light card in any room. */
.object-caption { --ink: var(--day-ink); --ink-muted: var(--day-ink-muted); --ink-faint: var(--day-ink-faint); --cave-focus: var(--day-ink); color: var(--ink); box-shadow: 0 2px 4px rgb(34 48 38 / .16); }

/* ---------- Object detail: take it to the window ----------
   Lifting something off the shelf is stepping to the window with it.
   The room dims to the wall's night tone (no blur, no card), the object
   lands in its own patch of window light on the left, and its words are
   written straight on the dim wall beside it. Every control is the same
   thing: light ink on the dim wall, a faint fill, 10px corners; they
   differ only in what they say. */
.item-overlay {
  --detail-wall: #111712; --detail-ink: #eef1ec; --detail-muted: #b8c3ba; --detail-faint: #94a097;
  --detail-fill: rgb(238 241 236 / .08); --detail-fill-hover: rgb(238 241 236 / .14); --detail-ring: #c3cdc4;
  position: fixed; z-index: 1000; inset: 0; overscroll-behavior: contain; touch-action: none;
}
.item-overlay[hidden] { display: none; }
.item-backdrop { position: absolute; inset: 0; background: rgb(17 23 18 / .975); opacity: 0; transition: opacity 260ms var(--ease-out); }
.item-overlay.is-open .item-backdrop { opacity: 1; }
/* The light comes back up while the object travels home, not after. */
.item-overlay.is-closing .item-backdrop { opacity: 0; transition: opacity 400ms var(--ease-out) 90ms; }
/* The words are on screen from the first frame. Motion only moves them. */
.item-dialog { --ink: var(--detail-ink); --ink-muted: var(--detail-muted); --ink-faint: var(--detail-faint); --cave-focus: var(--detail-ring);
  position: absolute; inset: 0; z-index: 1; display: grid; align-items: center; overflow: auto; overscroll-behavior: contain; touch-action: pan-y;
  padding: 96px max(48px, calc((100vw - 1200px) / 2)) 64px; color: var(--ink); pointer-events: none; }
.item-close, .item-preview, .item-copy { pointer-events: auto; }
.item-dialog:focus { outline: none; }
.item-overlay.is-closing .item-dialog__body .item-copy { opacity: 0; transform: translateX(16px); transition: opacity 140ms var(--ease-out), transform 140ms var(--ease-out); }
.item-dialog__body { display: grid; grid-template-columns: minmax(0, 1fr) minmax(320px, 420px); gap: clamp(48px, 6vw, 96px); align-items: center; min-height: 0; }
.item-stage { position: relative; display: grid; place-items: center; min-height: min(560px, 70dvh); }
/* the object's own patch of window light, from the same window */
.item-sun { position: absolute; inset: -8% -6% -4% 4%; pointer-events: none; filter: blur(18px); transform: skewX(-20deg); opacity: 0; transition: opacity 420ms var(--ease-out) 120ms; }
.item-sun i { position: absolute; width: 45%; background: var(--cave-sun); opacity: .2; }
.item-sun i:nth-child(1) { left: 10%; top: 0; height: 47%; } .item-sun i:nth-child(2) { left: 57%; top: 0; height: 47%; }
.item-sun i:nth-child(3) { left: 10%; top: 50%; height: 50%; opacity: .15; } .item-sun i:nth-child(4) { left: 57%; top: 50%; height: 50%; opacity: .15; }
.item-overlay.is-open .item-sun { opacity: 1; }
.item-overlay.is-closing .item-sun { opacity: 0; transition-delay: 0ms; transition-duration: 200ms; }
.item-preview { position: relative; min-width: 0; display: grid; place-items: center; perspective: 1000px; }

.item-close { position: fixed; z-index: 2; top: 28px; right: max(28px, calc((100vw - 1260px) / 2)); }
.item-close, .item-navigation button, .game-controls button {
  min-height: 44px; padding: 0 16px; border: 0; border-radius: 10px; background: var(--detail-fill); color: var(--detail-ink);
  font: inherit; font-size: 15px; font-weight: 600; cursor: pointer; transition: background-color 150ms var(--ease-out), scale 120ms var(--ease-out); }
.item-close:hover, .item-navigation button:hover, .game-controls button:hover { background: var(--detail-fill-hover); }
.item-close:active, .item-navigation button:active, .game-controls button:active { scale: .97; transition-duration: 60ms; }
.item-dialog :focus-visible, .game-renderer canvas:focus-visible { outline: 2px solid var(--detail-ring); outline-offset: 3px; }
.game-controls button:disabled { cursor: default; opacity: .5; }

.item-copy { min-width: 0; }
.item-kind { margin: 0 0 14px; font-size: 14px; font-weight: 500; color: var(--ink-muted); }
.item-copy h2 { margin: 0; font-size: clamp(30px, 2.8vw, 42px); line-height: 1.12; font-weight: 600; letter-spacing: -.035em; text-wrap: balance; }
.item-meta { margin: 12px 0 0; font-size: 17px; line-height: 1.45; color: var(--ink-muted); }
.item-stars { margin: 16px 0 0; font-size: 17px; letter-spacing: .08em; color: var(--ink); }
.item-stars .off { color: var(--ink-faint); }
#item-note { margin: 22px 0 0; max-width: 40ch; font-size: 17px; line-height: 1.65; }
.item-credit { margin: 22px 0 0; font-size: 13px; line-height: 1.5; color: var(--ink-faint); }
.game-art { display: block; margin: 22px 0 0; color: var(--ink-muted); text-decoration: none; }
.game-art img { display: block; width: 100%; border-radius: 6px; }
.game-art span { display: block; margin-top: 8px; font-size: 13px; }
.game-art:hover span { color: var(--ink); }
/* Neighbours by name: you can see where the arrow goes. */
.item-navigation { display: grid; grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr); align-items: center; gap: 12px; margin-top: 36px; }
.item-navigation button { display: flex; align-items: center; gap: 10px; min-width: 0; text-align: left; }
[data-item-next] { justify-content: flex-end; text-align: right; }
.item-navigation__name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 500; }
.item-navigation__arrow { flex: none; font-weight: 600; transition: translate 160ms var(--ease-out); }
[data-item-prev]:hover .item-navigation__arrow { translate: -3px 0; } [data-item-next]:hover .item-navigation__arrow { translate: 3px 0; }
.item-navigation__position { font-size: 14px; font-variant-numeric: tabular-nums; color: var(--ink-muted); white-space: nowrap; }

.preview-instruction { margin: 22px 0 0; font-size: 14px; line-height: 1.5; color: var(--ink-muted); text-align: center; }
.game-controls { display: flex; flex-wrap: wrap; justify-content: center; gap: 8px; margin-top: 14px; }
.game-label-note { margin: 10px 0 0; font-size: 13px; line-height: 1.5; color: var(--ink-faint); text-align: center; }
.item-preview .book3d { --bh: min(400px, 52dvh); }
.item-preview .device { width: min(100%, 520px); }
.item-preview .device-visual { height: min(380px, 48dvh); }
.item-preview .phone-visual { height: min(420px, 52dvh); }

@media (max-width: 960px) {
  /* On touch screens the whole sheet scrolls under a finger; "Put it back" closes it. */
  .item-dialog { align-items: start; padding: 100px 22px 32px; pointer-events: auto; }
  .item-close { top: 18px; right: 18px; }
  .item-dialog__body { grid-template-columns: 1fr; gap: 28px; }
  .item-stage { min-height: min(380px, 50dvh); }
  .item-preview .book3d { --bh: min(280px, 36dvh); }
  .item-preview .device-visual { height: min(260px, 34dvh); }
  .item-preview .phone-visual { height: min(300px, 38dvh); }
  .item-copy h2 { font-size: 28px; }
  .item-navigation { margin-top: 28px; }
}
@media (max-width: 600px) { .item-navigation { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); } .item-navigation__position { grid-row: 1; grid-column: 1 / -1; } }
/* A phone travels as a picture of itself, its image filling the box. */
.item-flight-object .phone-visual { height: 100%; }
.item-flight-object .phone-image { width: 100%; height: 100%; }
.item-flight-object .phone-body { display: none; }
/* The handheld frames already carry air above the machines. */
.collection-section--handhelds { padding-top: 0; }
/* The phone note is a small card pinned just above its shelf, not a heading. */
.collection-section--phones .collection-section__heading { margin-bottom: 8px; padding-inline: clamp(16px, 3vw, 40px); }
/* Putting it back: the object's own controls and hints go with the words. */
.item-overlay.is-closing :is(.game-controls, .preview-instruction, .game-label-note) { opacity: 0; transition: opacity 140ms var(--ease-out); }

/* A phone's picture needs a definite height, or its button collapses to
   0px wide: nothing to click, focus or fly. Width follows the photo. */
.phone-row .phone-image { height: calc(var(--phone-shelf-h) * var(--phone-scale, 1)); width: auto; }
.item-preview .phone-image { height: min(420px, 52dvh); width: auto; }
@media (max-width: 960px) { .item-preview .phone-image { height: min(300px, 38dvh); } }
/* First arrival: the window light comes up, then the first books settle
   onto their plank, left to right. Motion only: every object is in place
   and visible even if none of this plays. */
@media (prefers-reduced-motion: no-preference) {
  .cave-sun i { animation: cave-light-in 1100ms var(--ease-out) both; }
  @keyframes cave-light-in { from { opacity: 0; } }
  .book-row:first-child .collectible { animation: cave-settle 520ms var(--ease-out) backwards; }
  .book-row:first-child .collectible:nth-child(2) { animation-delay: 50ms; }
  .book-row:first-child .collectible:nth-child(3) { animation-delay: 100ms; }
  .book-row:first-child .collectible:nth-child(4) { animation-delay: 150ms; }
  .book-row:first-child .collectible:nth-child(5) { animation-delay: 200ms; }
  @keyframes cave-settle { from { translate: 0 -14px; } 70% { translate: 0 1px; } }
}
.collection-credit { max-width: min(62ch, 600px); }
/* The real 3D keep case, rendered at rest, standing on the shelf. The
   drawn case under it only shows for a game that has no render yet. */
.case-shot { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: contain; object-position: 50% 100%; filter: drop-shadow(5px 6px 7px var(--cave-book-cast)); pointer-events: none; }
.game.has-shot { aspect-ratio: 350 / 480; }
.game.has-shot .jewel-case { visibility: hidden; }
.item-flight-object .case-shot { position: static; object-fit: fill; }
