/* Catodoro landing - "The Book".
   One illustrated day bound as a real book: a hardcover, four two-page
   spreads, and colour-coded bookmarks down the fore edge. The book is laid
   out at its Figma size (1080 x 593) in real pixels and scaled as a whole
   with a single transform, so every inner measurement below is the exact
   number from the design and the 3D flip stays proportional at any size. */

:root {
  --ink: #1e0808;
  --cream: #fff8e7;
  --card: #fffefc;
  --paper: #f4f1ee;
  --sky-a: #b3ebff;
  --dusk-b: #b9a5e3;
  --night-b: #3a1a2e;
  --red: #d90429;
  --cover-red: #ad0101;
  --lime: #ccf49c;
  --sun: #fbd500;
  --display: 'Fuzzy Bubbles', cursive;
  --body: 'Quicksand', sans-serif;

  /* Book geometry, straight from the Figma frame */
  --page-w: 540px;
  --page-h: 593px;
  --book-w: 1080px;
  --tab-out: 37px;

  /* Motion. The camera runs a touch longer than the page turn and on a
     symmetric ease, so the world settles just after the leaf lands. */
  /* Timing ported from the CSS book reference: 0.9s on a symmetric
     ease-in-out. The page eases into the lift and settles out of it, rather
     than snapping away fast and coasting as an ease-out does. */
  --flip: 900ms;
  --ease: cubic-bezier(0.645, 0.045, 0.355, 1);
  /* Slower and softer than the page turn on purpose: a 100vh sky move at
     flip speed reads as a wipe. Long ease-out lets it drift to rest. */
  --cam-ms: 1500ms;
  --cam-ease: cubic-bezier(0.33, 0.06, 0.16, 1);
}

* { box-sizing: border-box; }

body {
  --chrome-ink: #1e0808;
  --chrome-soft: rgba(30, 8, 8, 0.62);
  margin: 0;
  height: 100vh;
  font-family: var(--body);
  color: var(--chrome-ink);
  background: var(--paper);
  line-height: 1.6;
  overflow: hidden;
}

/* The grass picks up each scene's light */
body[data-chapter='2'] { --grass-a: #c8f098; --grass-b: #9fd47a; }
body[data-chapter='3'] { --grass-a: #a9ce9a; --grass-b: #8bb882; }
body[data-chapter='4'] { --grass-a: #b2d0a9; --grass-b: #8fb489; }

/* The night scene turns the whole page dark, so the chrome inverts with it */
body[data-chapter='5'] {
  --chrome-ink: #fff8e7;
  --chrome-soft: rgba(255, 248, 231, 0.72);
}

/* ---------- The world behind the book ----------

   The five backdrops are five camera stops on one canvas laid out as a
   3 x 2 grid of viewports:

       (0,0) cover        (2,0) night
       (0,1) canvas  (1,1) payday  (2,1) ledger

   Moving between stops pans .scene, so props that straddle two cells - the
   hill, the tree, the moon - carry across a transition by themselves. That
   is what makes the elements "match": there is only ever one of each. */

.world {
  position: fixed;
  inset: 0;
  z-index: 0;
  overflow: hidden;
  pointer-events: none;
}

/* One unbroken gradient across both cells of column 0. The cover's bottom
   (#a2e7f2) and the daylight's top (#a8e7fc) meet at 50%, so there is no
   colour step at the fold - the camera just travels down through it. */
.sky-col {
  position: absolute;
  left: 0;
  top: 0;
  width: 300vw;
  height: 200vh;
  transform: translate3d(calc(var(--cam-x, 0) * -100vw), calc(var(--cam-y, 0) * -100vh), 0);
  /* Single averaged stop where the two scenes meet. Two stops at the same
     position would be a hard step, and the pan parks it near BG2's top edge
     - that was the sharp line in the daylight sky. */
  background: linear-gradient(
    180deg,
    #fce9c7 0%, #f4e9cd 7%, #eae9d3 13%, #dee9db 19%,
    #cfe9e3 26%, #e0f2ef 34%, #d2f1f2 43%, #a5e7f7 52%,
    #b8ebfc 60%, #caf1ff 69%, #dbf5ff 78%,
    #eaf9ff 88%, #f6fdff 100%
  );
}

.is-ready .sky-col { transition: transform var(--cam-ms) var(--cam-ease); }
.sky-row {
  position: absolute;
  left: 0;
  top: 0;
  width: 300vw;
  height: 200vh;
  /* Same canvas space as .sky-col: the row's own offset lives on the panels,
     never in the camera term. Baking it in here cancelled the translation
     and parked the whole row outside the clipped world. */
  transform: translate3d(calc(var(--cam-x, 0) * -100vw), calc(var(--cam-y, 0) * -100vh), 0);
}

.is-ready .sky-row { transition: transform var(--cam-ms) var(--cam-ease); }

/* Each scene keeps its own gradient across the bulk of its cell and blends
   only in a narrow band at the seam. The panels are stacked rather than
   merely overlapped: dusk sits underneath payday, so where payday fades out
   it reveals dusk - not the base sky. Fading both toward transparent would
   let the daylight blue leak through the join. */
.sky-panel {
  position: absolute;
  top: 100vh;
  height: 100vh;
}

/* Dusk: solid, and reaching back under payday's right edge. Stops read down
   the right edge of the design's own BG 4 render, which is the lower half
   here - the panel runs the full 200vh of the column so that panning up into
   the night has dusk sky above it to travel through. Without that the pan
   lifted dusk's top edge off the screen and showed the daylight blue of the
   base column behind it, which was the cut. The upper half is an invented
   continuation: it is only ever seen mid-transition, under the night. */
.sp-3 {
  left: 185vw;
  width: 125vw;
  top: 0;
  height: 200vh;
  z-index: 1;
  background: linear-gradient(
    180deg,
    #fdd3dc 0%, #fbdade 25%, #f9e0e1 50%,
    #f1dfe3 55%, #eadde5 60%, #e2d9e5 65%,
    #dbd8e6 70%, #d7d6e7 75%, #d4d6e9 80%, #d1d6ea 85%,
    #ced5ea 90%, #cdd6ec 95%, #cdd7ec 100%
  );
}

/* Night: the same column, the same pan, cross-fading over dusk. Night and
   dusk are two times rather than two places, so they dissolve into each
   other - but both are panning together now, so there is no edge sliding
   past and nothing behind either of them to leak through. */
.sp-night {
  left: 185vw;
  width: 125vw;
  top: 0;
  height: 200vh;
  z-index: 3;
  opacity: 0;
  background: linear-gradient(
    180deg,
    #1e0808 0%, #20090b 5%, #230b0f 10%, #260d13 15%,
    #2a1018 20%, #2e141e 25%, #331823 30%, #3a1e2b 35%,
    #422533 40%, #4b2e3c 45%, #533645 50%,
    #5c4050 62%, #654a5a 76%, #6c5263 88%, #715869 100%
  );
  transition: opacity var(--cam-ms) var(--cam-ease);
}

body[data-chapter='5'] .sp-night { opacity: 1; }

/* The closing screen's daylight, read down the left edge of BG 6. It sits
   above the night panel and comes in as the wheel turns. */
.sp-day {
  left: 185vw;
  width: 125vw;
  top: 0;
  height: 200vh;
  z-index: 4;
  opacity: 0;
  background: linear-gradient(
    180deg,
    #aae8ff 0%, #b2ebff 5%, #bdeeff 10%, #e4f7fc 15%,
    #e8f8fc 20%, #dff5fd 25%, #ddf6ff 30%, #e5f8ff 35%,
    #edfaff 40%, #f6fcff 45%, #fefeff 50%,
    #fefeff 100%
  );
  transition: opacity 2200ms cubic-bezier(0.4, 0.05, 0.2, 1);
}

body[data-chapter='6'] .sp-day { opacity: 1; }

/* Payday: on top, fading in over the daylight blue on its left and out over
   dusk on its right. Solid for the middle ~60vw of its cell. */
.sp-2 {
  left: 85vw;
  width: 115vw;
  z-index: 2;
  background: linear-gradient(180deg, #fbe9c1 0%, #fcedd8 18%, #fce3cf 38%, #fce2d6 56%, #fbe0db 76%, #fae0df 100%);
  -webkit-mask-image: linear-gradient(90deg, transparent 0, #000 26%, #000 87%, transparent 100%);
  mask-image: linear-gradient(90deg, transparent 0, #000 26%, #000 87%, transparent 100%);
}
.scene {
  position: absolute;
  top: 0;
  left: 0;
  width: 300vw;
  height: 200vh;
  transform: translate3d(calc(var(--cam-x, 0) * -100vw), calc(var(--cam-y, 0) * -100vh), 0);
  will-change: transform;
}

/* Camera transitions only after boot, so a deep link to #ch3 lands on its
   stop instead of flying there from the cover. */
.is-ready .scene,
.is-ready .glows,
.is-ready .clouds-front { transition: transform var(--cam-ms) var(--cam-ease); }

.scene > * { position: absolute; }

/* --- the morning glows, straight from the design --- */

/* Deliberately NOT panned. These are huge circles, so sliding them 100vh
   swept their hard arcs up the screen - that was the wipe. They stay put
   and simply fade, well ahead of the camera. */
.glows {
  position: absolute;
  inset: 0;
  mix-blend-mode: multiply;
}

/* Asymmetric on purpose. Leaving the cover they go straight away, before the
   pan builds up. Arriving back, they wait for the camera to finish: these
   are static hard-edged ellipses, and fading them in over a sky that is
   still sliding underneath reads as a fixed cut line across the screen. */
.is-ready .glows { transition: opacity 520ms ease-in 1080ms; }
body:not([data-chapter='0']):not([data-chapter='1']) .glows {
  opacity: 0;
  transition-duration: 380ms;
  transition-delay: 0ms;
}

.glow {
  position: absolute;
  left: 50vw;
  border-radius: 50%;
  filter: blur(50px);
}

/* Hues and geometry are the design's. The alphas are raised roughly 2.5x:
   the designed values multiply over a near-white frame, but here they land
   on an already-tinted sky, where they were invisible. */
.glow-1 {
  width: 92.15vw;
  height: 92.15vw;
  margin-left: -46.08vw;
  top: calc(120.7vh - 46.08vw);
  background: linear-gradient(180deg, rgba(0, 197, 251, 0.26) 0%, rgba(0, 197, 251, 0) 100%);
}

.glow-2 {
  width: 105.9vw;
  height: 105.9vw;
  margin-left: -52.95vw;
  top: calc(122.6vh - 52.95vw);
  background: linear-gradient(180deg, rgba(82, 255, 220, 0.36) 0%, rgba(82, 255, 220, 0) 100%);
}

.glow-3 {
  width: 114.9vw;
  height: 114.9vw;
  margin-left: -57.45vw;
  top: calc(120.6vh - 57.45vw);
  background: linear-gradient(180deg, rgba(255, 235, 180, 0.55) 0%, rgba(255, 235, 180, 0) 100%);
}

/* --- sun and clouds: the exported artwork, grain and all --- */

/* Transparent artwork straight from Figma - no mask or blend needed */
.sun {
  left: 50vw;
  top: -2.7vh;
  width: 11.35vw;
  height: 11.35vw;
  margin-left: -5.67vw;
}

.cloud {
  height: auto;
  pointer-events: none;
}

/* cl-2 and cl-3 do double duty: they sit along the cover's bottom edge, and
   the same two clouds become the daylight scene's top clouds once the camera
   pans down. BG2 has no clouds of its own - these are them. */
.cl-1 { left: -8vw; top: 46vh; width: 32vw; }
.cl-2 { left: -4vw; top: 88vh; width: 30vw; }
.cl-3 { left: 76vw; top: 92vh; width: 26vw; }

/* --- foreground clouds, above the book --- */

.clouds-front {
  position: fixed;
  inset: 0;
  z-index: 2;
  overflow: hidden;
  pointer-events: none;
  transform: translate3d(calc(var(--cam-x, 0) * -100vw), calc(var(--cam-y, 0) * -100vh), 0);
  transition: transform var(--cam-ms) var(--cam-ease);
}

.clouds-front .cloud { position: absolute; }

/* Low along the cover's bottom edge, clear of the book at rest. They are a
   cover-scene device, so they retire on the way out rather than being
   dragged up into the daylight sky. */
.cl-left  { left: -14vw; top: 74vh; width: 56vw; }
.cl-right { left: 60vw;  top: 71vh; width: 52vw; }
.cl-under { left: 25vw;  top: 86vh; width: 48vw; }

/* Same asymmetry: gone quickly on the way out, held back until the camera
   has nearly settled on the way in. */
.is-ready .clouds-front {
  transition: transform var(--cam-ms) var(--cam-ease), opacity 560ms ease-in 940ms;
}

body:not([data-chapter='0']):not([data-chapter='1']) .clouds-front {
  opacity: 0;
  transition-duration: var(--cam-ms), 420ms;
  transition-delay: 0ms, 0ms;
}

/* --- the hill: one element across all three ground cells --- */

/* One 1440x138 strip per ground cell, sitting flush on the cell's bottom
   edge. Width is fixed to the viewport so the grass line meets exactly at
   the seams between cells. */
.hill {
  width: 132vw;
  height: auto;
  top: calc(200vh - 12.65vw);
}

/* Overlapped and feathered at the join, so the grass colour changes across
   the seam rather than at it. hill-2 also fades back out on its right, into
   the terminal strip - the two profiles cross at 200vw, which is where the
   handover is placed so no second ridge line shows through. */
.hill-2 {
  -webkit-mask-image: linear-gradient(90deg, transparent 0, #000 16%, #000 79.5%, transparent 84.1%);
  mask-image: linear-gradient(90deg, transparent 0, #000 16%, #000 79.5%, transparent 84.1%);
}

/* hill-23 draws its grass line a touch higher in the frame than hill-1, so
   the later strips drop to meet it and the horizon runs level across cells. */
.hill-1 { left: -5vw; }
.hill-2 { left: 95vw;  top: calc(200vh - 12.65vw + 0.7vw); }

/* The Hill component at full width (943 x 138), kept at the same pixel scale
   as the strips before it: 943 * 132/1440 = 86.4vw. Its grass runs out of the
   bottom of the world 72.4vw along, so left 180.6vw puts that at 253vw - 53%
   across the ledger's viewport, where the design ends the grassland. */
.hill-end {
  left: 180.6vw;
  top: calc(200vh - 12.65vw + 0.7vw);
  width: 86.4vw;
  /* The two strips hand over at 200vw, where their grass lines cross within
     a pixel. Each fade completes before the other starts, so the pair is
     never both semi-transparent at once - a straight cross-fade would open a
     hole and let the sky through the middle of the grass. */
  -webkit-mask-image: linear-gradient(90deg, transparent 15.5%, #000 22.5%, #000 100%);
  mask-image: linear-gradient(90deg, transparent 15.5%, #000 22.5%, #000 100%);
}

/* --- the tree: parked on the seam between payday and the ledger, so it
       leaves bg3 on the right and arrives in bg4 on the left --- */

/* Centred on 200vw: half of it lives in each cell, which is what makes it
   the same tree crossing rather than two trees. Base is tucked a little
   into the grass so it is planted rather than floating. */
/* Figma parks tree-canvas at the same x in both bg3 and bg4 (1269 and -171 of
   a 1440 frame, one frame apart), which puts its centre a shade right of the
   seam. Sized and raised to match that render; the extra depth into the grass
   over the design is this scene's slightly higher horizon. */
.tree {
  top: calc(200vh - 12.65vw - 24.75vw);
  width: 26vw;
  height: auto;
}

.tree { left: 188.1vw; }

/* --- the moon: sits above bg4's viewport and descends into bg5 --- */

/* Parked at its bg5 position, then lifted clear of the viewport for every
   earlier stop. The camera moves 100vh between the ledger and the night;
   the moon rides that plus its own 70vh, so it sweeps down into frame
   instead of simply sliding in with the rest of the scene. */
/* Fitted from three chords of the design's BG 5 render: a circle of radius
   735 centred at (719, 1307) of a 1440x900 frame, which is the 1480px Moon
   ellipse the file carries. Flat --cream with a hard edge - the render has
   no shading inside it at all, just a faint lift in the sky either side,
   which is the box-shadow. Both axes are in vw so it stays a true circle.

   In BG 4 the same ellipse sits at frame y -1632, a whole screen above the
   viewport. That is 144.3vh higher in world terms than its BG 5 place, so
   the moon carries its own travel on top of the camera's 100vh - which is
   exactly what makes it sweep down into the night rather than slide in with
   the rest of the scene. It runs longer and eases out harder than the
   camera, so a body that size settles rather than stops. */
/* ---------- The sun-and-moon dial ----------

   Both bodies ride one wheel whose hub sits far below the night cell, at
   (250vw, 300vh). That distance is the whole trick: with a near hub the sun
   would have had a radius of ~10vh and barely moved. Out here the moon runs
   on a 202vh radius and the sun on 160.6vh, so a 40deg turn sweeps each of
   them more than 100vh sideways - the moon setting to the left while the sun
   comes up on the right, one rotation, no cross-fade.

   Angles are chosen so that at rest the moon is straight up from the hub
   (its 11:59 position) and the sun is 40deg round to the right, off-screen.
   Turning the wheel -40deg lands the sun where the design puts it and
   carries the moon out of frame. */
.dial {
  position: absolute;
  left: 250vw;
  top: 300vh;
  width: 0;
  height: 0;
  transform: rotate(var(--dial, 0deg));
  transform-origin: 0 0;
  transition: transform 2400ms cubic-bezier(0.36, 0.06, 0.16, 1);
}

body[data-chapter='6'] .dial { --dial: -40deg; }

/* A slot is just a point on the rim; its body hangs centred on it. */
.dial-slot {
  position: absolute;
  left: 0;
  top: 0;
  width: 0;
  height: 0;
}

.slot-moon { transform: rotate(0deg) translateY(-202vh); }
.slot-sun  { transform: rotate(40deg) translateY(-202vh); }

/* The design's own sunrise, corona ring and rim cats included. Its solid
   inner disc is 0.6841 of the file and concentric with it, so 54.82vw of
   image puts that disc at 37.5vw - the moon's exact diameter. Both bodies
   now sit on the same rim at the same size, 40deg apart, so the turn swaps
   one for the other rather than trading a small moon for a huge sun. */
.sun-rise {
  position: absolute;
  left: -27.41vw;
  top: -27.41vw;
  width: 54.82vw;
  height: 54.82vw;
  /* Off the wheel entirely until the night: parked at 40deg it still hangs
     at 177vh, which the ledger's viewport (100-200vh) can see the left edge
     of. Nothing to see there, so it simply is not drawn. */
  opacity: 0;
}

body[data-chapter='5'] .sun-rise,
body[data-chapter='6'] .sun-rise { opacity: 1; }

/* Sized so the camera can pass it. The design's own moon is 163vh tall and
   travels 230vh, which means it can never be seen whole - it goes straight
   from off the top to a cropped arc at the bottom, covering the entire
   screen on the way. At 37.5vw across (60vh at this aspect) the disc fits
   inside the viewport with room either side, so partway through the move
   the complete moon is on screen, and it still parks low enough that only
   its cap shows once the night settles.

   The asset is 2557px square with its solid disc 1477 of that - 57.76% -
   so the image runs 64.9vw to land a 37.5vw disc, and the rest is the
   baked halo. Its edge is hard in the file, as the design's render has it.

   Hidden above the ledger at -40vh, resting with its crown at 68vh in the
   night: 140vh of screen travel, of which the middle 29% has the whole
   moon in frame. It runs a little longer than the camera so it is the last
   thing to settle. */
.moon-big {
  /* Explicit now it lives on the dial: `.scene > *` no longer reaches it,
     and without this its offsets are ignored and it drifts off-screen. */
  position: absolute;
  left: -32.45vw;
  top: -32.45vw;
  width: 64.9vw;
  height: auto;
  transform: translateY(var(--moon-y, -40vh));
  transition: transform 1900ms cubic-bezier(0.3, 0.08, 0.2, 1);
}

body[data-chapter='5'] .moon-big { --moon-y: 0vh; }

/* --- birds --- */

.bird { width: 2.4vw; height: 1vw; opacity: 0.72; }
.bird-1 { left: 21vw;  top: 21vh; }
.bird-2 { left: 74vw;  top: 18vh; }
.bird-3 { left: 175vw; top: 105vh; }
.bird-4 { left: 195vw; top: 126vh; }
/* bg4 puts a bird at 7.4% across, level with bird-4 - the design re-places
   the same one after the pan, so here they read as a pair either side of the
   seam and each viewport still gets one. */
.bird-5 { left: 207.4vw; top: 125.8vh; }

/* Everything else sits above the world */
.navbar,
main {
  position: relative;
  z-index: 1;
}

h1, h2 { font-family: var(--display); font-weight: 700; margin: 0; }
p { margin: 0; }
a { color: inherit; }

.sr-only {
  position: absolute;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
  border: 0;
}

:focus-visible {
  outline: 3px solid var(--red);
  outline-offset: 3px;
  border-radius: 6px;
}

/* The key chip is chrome.css's, shared with the shortcuts page. This file
   used to carry its own - heavier, 0.8em, a hard 2px ink border - and since
   it loads after chrome.css it won outright, which is why the hint's arrows
   never matched the ones they were meant to match. */

/* ---------- Chrome ---------- */

/* Fixed pill nav, 474 x 60 centred at the top, exactly as designed */
.navbar {
  position: fixed;
  top: 28px;
  left: 50%;
  transform: translateX(-50%);
  z-index: 5;
  /* Shrink-to-fit on its contents, not on what is left of the viewport.
     Fixed at left: 50% the available width is only half the screen, so once
     the bar is allowed to wrap it folded into three rows on a phone rather
     than the two it needs. page.css has always carried this. */
  width: max-content;
  display: flex;
  align-items: center;
  gap: 4px;
  height: 60px;
  /* Even on both sides: the left used to carry 26px to optically offset a
     plain text link against the filled CTA, but with the current page now
     drawn as its own pill the two ends are the same shape and the extra
     just read as lopsided. */
  padding: 0 13px;
  background: var(--card);
  border-radius: 999px;
  box-shadow: 0 6px 22px rgba(30, 8, 8, 0.13);
  max-width: calc(100vw - 2rem);
}

.nav-item {
  padding: 6px 13px;
  font-weight: 700;
  font-size: 15px;
  letter-spacing: 0;
  text-decoration: none;
  color: var(--ink);
  border-radius: 999px;
  white-space: nowrap;
  transition: background 0.16s ease;
}

.nav-item:hover { background: rgba(30, 8, 8, 0.06); }

/* The page you are already on carries no styling of its own - it is still
   marked up with aria-current for anyone navigating by structure. */

.nav-cta {
  /* Matches a nav item's own padding, so the air before the button is
     the same as the air between the links rather than 20px more. */
  margin-left: 13px;
  padding: 8px 17px;
  font-weight: 700;
  font-size: 14px;
  letter-spacing: 0;
  text-decoration: none;
  color: var(--cream);
  background: var(--red);
  border-radius: 999px;
  white-space: nowrap;
  transition: transform 0.14s ease, box-shadow 0.14s ease;
  box-shadow: 0 3px 10px rgba(217, 4, 41, 0.32);
}

.nav-cta:hover { transform: translateY(-1px); box-shadow: 0 5px 14px rgba(217, 4, 41, 0.4); }


/* ---------- Narrow screens ----------

   Five items at full size need 397px and a phone gives the bar 358px at most,
   so something has to yield. Wrapping the button onto its own row was worse:
   it left the links and the button on different lines with nothing lining up.
   Shrinking the type keeps all five on one row, where `align-items: center`
   holds them to a single axis. Measured, not guessed - at 12px the bar comes
   to 319px, which clears every common width from 360 up, and the tighter side
   margin below covers 320 as well. */
@media (max-width: 480px) {
  .navbar {
    height: 50px;
    padding: 0 8px;
    gap: 2px;
    /* 1rem of margin rather than 2: on a 320px screen those 16px are the
       difference between one row and two. */
    max-width: calc(100vw - 1rem);
  }

  .nav-item {
    padding: 6px;
    font-size: 12px;
  }

  .nav-cta {
    margin-left: 6px;
    padding: 8px 11px;
    font-size: 12px;
  }
}

/* The very smallest phones still in use. 12px needs 319px and a 320 screen
   leaves the bar 297, so one more step down - measured the same way, this
   comes to 286. */
@media (max-width: 344px) {
  .navbar { padding: 0 6px; }

  .nav-item {
    padding: 6px 4px;
    font-size: 11.5px;
  }

  .nav-cta {
    padding: 8px 9px;
    font-size: 11.5px;
  }
}

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-family: var(--display);
  font-weight: 700;
  font-size: 1rem;
  text-decoration: none;
  border: 3px solid var(--ink);
  border-radius: 999px;
  padding: 0.55rem 1.4rem;
  background: var(--card);
  box-shadow: 0 4px 0 var(--ink);
  transition: transform 0.12s ease, box-shadow 0.12s ease;
}

.btn:hover { transform: translateY(2px); box-shadow: 0 2px 0 var(--ink); }
.btn-red { background: var(--red); color: var(--cream); }
.btn-small { font-size: 0.85rem; padding: 0.35rem 1rem; border-width: 2.5px; box-shadow: 0 3px 0 var(--ink); }

/* The book is centred in the viewport and the chrome floats over it */
main {
  height: 100vh;
  display: grid;
  place-items: center;
}

.hint {
  position: fixed;
  left: 50%;
  bottom: 22px;
  transform: translateX(-50%);
  z-index: 4;
  /* Sizes to its own longest line, not to what is left of the viewport.
     Fixed at left: 50% the available width is only half the screen, so at
     900px the pill capped at 450 - four pixels under the first line - and
     the forced break turned into three lines instead of two. Same trap the
     navbar was in. */
  width: max-content;
  text-align: center;
  font-weight: 500;
  font-size: 0.9rem;
  color: var(--chrome-soft);
  /* Roomier than the body default so the key chips sit in the line rather
     than pushing out of the pill: 0.9rem x 2 clears their 26.5px. */
  line-height: 2;
  transition: color var(--cam-ms) var(--cam-ease), background var(--cam-ms) var(--cam-ease);
  max-width: min(40rem, calc(100vw - 2rem));
  /* Legible over grass, dusk or night alike */
  background: var(--hint-veil, rgba(255, 254, 252, 0.55));
  /* A card, not a pill. Two lines of text in a 999px radius already read as
     a lozenge before the line count climbed on a phone - this is the value
     the narrow-screen rule was correcting to, applied everywhere. */
  border-radius: 18px;
  padding: 6px 18px;
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
}

/* On a phone the line wraps four or five deep, and a 999px radius on a box
   that tall stops reading as a pill and starts reading as a circle. It
   becomes a card instead, and the keyboard clause goes wherever there is no
   keyboard to press. */
/* The break is only an improvement while the longer half still fits a line of
   its own. Below the smaller type it takes about 412px, so it survives well
   past the sizing breakpoint above; narrower than this it would force one
   long line and one short, and the text goes back to wrapping where it lands. */
@media (max-width: 470px) {
  .hint-br { display: none; }
}

@media (max-width: 640px) {
  .hint {
    padding: 10px 16px;
    font-size: 0.82rem;
    line-height: 1.75;
    bottom: 16px;
  }
}

@media (hover: none) and (pointer: coarse) {
  .hint-keys { display: none; }
}

/* Two adjacent inline-blocks are a legal place to break a line, so at narrow
   widths the pair split and the right arrow dropped onto a line by itself. */
.hint-keys { white-space: nowrap; }


body[data-chapter='5'] { --hint-veil: rgba(30, 8, 8, 0.4); }
body[data-chapter='5'] .nav-btn { color: var(--cream); background: rgba(30, 8, 8, 0.42); }

/* The chip is chrome.css's, unmodified - the same one the shortcuts page
   uses. It stands 26.5px tall, so the line box has to be at least that or it
   breaks out of the pill. */

.back-link {
  position: fixed;
  left: 20px;
  bottom: 20px;
  z-index: 4;
  font-weight: 600;
  font-size: 0.74rem;
  color: var(--chrome-soft);
  text-decoration: none;
  opacity: 0.75;
  transition: color var(--cam-ms) var(--cam-ease), opacity 0.16s ease;
}

.back-link:hover { opacity: 1; text-decoration: underline; }

/* ---------- Stage ---------- */

.stage {
  --scale: 1;
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: clamp(0.25rem, 2vw, 1.5rem);
  padding: clamp(1rem, 3vw, 2.2rem) clamp(0.5rem, 3vw, 2rem);
  cursor: pointer;
  user-select: none;
  -webkit-user-select: none;
}

/* A transform never changes layout size, so the slot carries the *scaled*
   footprint and the book is scaled inside it from its top-left corner.
   Perspective lives here, and is deliberately NOT scaled: scale() leaves the
   Z axis alone, so a constant depth keeps the flip identical at every size. */
.book-slot {
  position: relative;
  flex: 0 0 auto;
  width: calc(var(--book-w) * var(--scale));
  height: calc(var(--page-h) * var(--scale));
  /* The bookmarks overhang the right edge, so that width has to be reserved
     - but reserving it on one side only makes flex centre the spread plus
     its overhang, which pushes the spread itself off-centre. Matching the
     reservation on the left centres the 1080 spread and lets the tabs hang
     into the right margin, which is how the design frames it. */
  margin-left: calc(var(--tab-out) * var(--scale));
  margin-right: calc(var(--tab-out) * var(--scale));
  /* The reference runs perspective 1200px against a 360px page - a depth
     ratio of ~3.3. Matched here against this book's 540px page, which is a
     good deal more dramatic than the 4.8 ratio I had. */
  perspective: 1800px;
  perspective-origin: 50% 46%;
}

/* Scaling and the open/closed shift are deliberately on two elements: the
   shift is animated with the page turn, the scale must never be - otherwise
   the book zooms in on load and lags behind every window resize. */
.book-scale {
  position: absolute;
  top: 0;
  left: 0;
  width: var(--book-w);
  height: var(--page-h);
  transform-origin: 0 0;
  transform-style: preserve-3d;
  transform: scale(var(--scale));
}

.book {
  position: relative;
  width: 100%;
  height: 100%;
  /* Pinned to the ink: the printed page never follows the chrome theme,
     even when the backdrop turns the rest of the site dark. */
  color: var(--ink);
  transform-style: preserve-3d;
  transform: translateX(var(--shift, 0px));
  /* No filter / opacity / clip on this element: any grouping property here
     would force transform-style back to flat and kill the 3D flip. The
     book's ground shadow lives on the paper block below instead. */
}

/* Closed, only the right half is a page, so slide the block left by half a
   page to keep the cover centred in the slot. */
.book.is-closed { --shift: -270px; }

/* Transitions only after the first measured layout, so the book does not
   animate into place on load or fight a window resize. */
.is-ready .book { transition: transform var(--flip) var(--ease); }
.is-ready .leaf { transition: transform var(--flip) var(--ease); }
.is-ready .base-left { transition: opacity calc(var(--flip) * 0.5) linear; }

/* ---------- Paper block behind the leaves ---------- */

.base {
  position: absolute;
  top: 0;
  width: var(--page-w);
  height: var(--page-h);
  background: var(--card);
  /* Clear of the deepest turned leaf, which sits at -(LAST * 1.5px). With
     six leaves that is -7.5px, so the old -6px put the block in front of a
     turned first leaf and the open left page went blank. */
  transform: translateZ(-14px);
  box-shadow: 2px 10px 26px rgba(30, 8, 8, 0.2);
}

.base-left { left: 0; border-radius: 3px 0 0 3px; }
.base-right { left: var(--page-w); border-radius: 0 3px 3px 0; }
.book.is-closed .base-left { opacity: 0; }
.book.is-closed .base-right { border-radius: 3px 16px 16px 3px; }

/* ---------- The turning leaves ---------- */

.leaf {
  position: absolute;
  top: 0;
  left: var(--page-w);
  width: var(--page-w);
  height: var(--page-h);
  transform-origin: left center;
  transform-style: preserve-3d;
  transform: rotateY(0deg) translateZ(var(--lz, 0px));
}

.leaf.is-turned { transform: rotateY(-180deg) translateZ(var(--lz, 0px)); }

/* No clip and no radius here, deliberately. This is the element doing the
   backface culling, and a grouping property on it - overflow, opacity, a
   filter - makes Firefox render it as a flattened layer that has dropped out
   of the leaf's 3D context, so the culling stops applying: both faces paint,
   and a turned leaf shows its front through the back, mirrored. Chrome is
   forgiving about this; Firefox is not. .page fills the face and already
   clips, so nothing was gained by clipping twice. */
.face {
  position: absolute;
  inset: 0;
  backface-visibility: hidden;
  -webkit-backface-visibility: hidden;
}

/* The two faces are otherwise coplanar, which invites z-fighting on top of
   the culling question. A hair of Z puts each one proud of its own side of
   the sheet - applied after the rotation, so the back face's push is away
   from the viewer on the back. */
.face-front { transform: translateZ(0.06px); }
.face-back { transform: rotateY(180deg) translateZ(0.06px); }

/* Closed, the cover rounds at 13.5px while a face clips at 3px, so the
   cover's curve cuts a notch and the leaf immediately behind it fills that
   notch with a square corner. Matching the radius while the book is shut
   puts every leaf inside the cover's silhouette; they go back to the page's
   own 3px as soon as it opens, which is hidden under the turning cover. */
.book.is-closed .page,
.book.is-closed .base-right {
  border-radius: 13.5px;
}








/* Corner peel - the affordance that says "this page turns" */
.peel {
  position: absolute;
  right: 0;
  bottom: 0;
  width: 44px;
  height: 44px;
  pointer-events: none;
  opacity: 0;
  background: linear-gradient(315deg, rgba(30, 8, 8, 0.22), rgba(30, 8, 8, 0) 62%);
  transition: opacity 0.25s ease, width 0.25s ease, height 0.25s ease;
}

/* Only shown when a forward turn is actually available */
.stage[data-zone='next'] .leaf.is-top .face-front .peel {
  opacity: 1;
  width: 76px;
  height: 76px;
}

/* ---------- Bookmarks ---------- */

.tabs { transform-style: preserve-3d; }

.tab {
  position: absolute;
  right: calc(var(--tab-out) * -1);
  width: var(--tab-out);
  height: 90px;
  margin: 0;
  padding: 16px 8px;
  justify-content: center;
  border: 0;
  border-radius: 0 4px 4px 0;
  /* Required for the root's backface-visibility to mean anything: without
     it the tab flattens its children into its own plane. */
  transform-style: preserve-3d;
  background: var(--tab, var(--card));
  color: var(--tab-ink, var(--ink));
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: flex-end;
  transform: translateZ(0.5px);
  box-shadow:
    inset 0 4.49px 4.49px rgba(255, 255, 255, 0.14),
    2px 4px 5px rgba(30, 8, 8, 0.1);
  /* Everything that differs between the two edges is animated, so a tab
     crossing the book never snaps its label, rounding or shading. */
  transition:
    transform 520ms var(--ease),
    border-radius 520ms var(--ease),
    box-shadow 520ms var(--ease),
    filter 0.18s ease;
}

.tab::before {
  content: '';
  position: absolute;
  right: 100%;
  top: 0;
  bottom: 0;
  width: calc(68px - var(--tab-out));
  background: inherit;
  box-shadow: inset 0 4.49px 4.49px rgba(255, 255, 255, 0.14);
  /* Glued to this face of the page - so it is gone the moment the leaf
     turns far enough to show its reverse. */
  backface-visibility: hidden;
  -webkit-backface-visibility: hidden;
}

.tab span {
  backface-visibility: hidden;
  -webkit-backface-visibility: hidden;
  transition: transform 520ms var(--ease);
  writing-mode: vertical-rl;
  font-family: var(--display);
  font-weight: 700;
  font-size: 14px;
  line-height: 1.4;
  letter-spacing: 1.4px;
  text-transform: uppercase;
  white-space: nowrap;
}

.leaf:not(.is-turned) .tab:not(.is-active):hover {
  filter: brightness(1.06);
  transform: translateZ(0.5px) translateX(5px);
}

/* The current chapter's tab rides in front of the page - keeping its own
   colour. (11:30 AM's colour simply is paper white.) */
.tab.is-active { cursor: default; }

/* A chapter you have already read keeps its slot but crosses to the fore
   edge on the left, tucked behind the turned leaves. */
/* No transform of its own once turned - the leaf carries it across. The tab
   only counter-rotates so its label stays readable on the left fore edge,
   and re-mirrors its rounding and shading to suit that side. */
.leaf.is-turned .tab {
  border-radius: 4px 0 0 4px;
  box-shadow:
    inset 0 4.49px 4.49px rgba(255, 255, 255, 0.14),
    -2px 4px 5px rgba(30, 8, 8, 0.1);
}

.leaf.is-turned .tab::before {
  content: '';
  position: absolute;
  right: 100%;
  top: 0;
  bottom: 0;
  width: calc(68px - var(--tab-out));
  background: inherit;
  box-shadow: inset 0 4.49px 4.49px rgba(255, 255, 255, 0.14);
  /* Glued to this face of the page - so it is gone the moment the leaf
     turns far enough to show its reverse. */
  backface-visibility: hidden;
  -webkit-backface-visibility: hidden;
}


/* Lightened with a full-bleed inset rather than `filter: brightness()`.
   A filter is a grouping property: it flattens the tab, which takes its
   label out of the leaf's 3D context and stops backface-visibility culling
   it - so hovering a bookmark on an already-turned leaf brought the text
   back, mirrored. The note on .tab's own transform-style says exactly this;
   the filter undid it. A box-shadow groups nothing. */
.leaf.is-turned .tab:hover {
  box-shadow:
    inset 0 0 0 200px rgba(255, 255, 255, 0.16),
    inset 0 4.49px 4.49px rgba(255, 255, 255, 0.14),
    2px 4px 5px rgba(30, 8, 8, 0.1);
}

/* Five slots now, at the design's own tops and heights - each one laps a
   few pixels over the tab above it, which is what stacks them. */
.tab-1 { top: 29px;  height: 94px; --tab: var(--lime); }
.tab-2 { top: 110px; height: 94px; --tab: var(--card); }
.tab-3 { top: 196px; height: 91px; --tab: var(--sun); }
.tab-4 { top: 279px; height: 89px; --tab: var(--dusk-b); }
.tab-5 { top: 360px; height: 90px; --tab: var(--night-b); --tab-ink: var(--card); }

/* ---------- Paper ---------- */

.page {
  position: absolute;
  inset: 0;
  background: var(--card);
  overflow: hidden;
  /* Carries the page's rounded fore edge now that .face cannot clip. */
  border-radius: 0 3px 3px 0;
}

.page::before {
  content: '';
  position: absolute;
  inset: 0;
  background: url('assets/paper-texture.jpg') center / cover no-repeat;
  mix-blend-mode: multiply;
  pointer-events: none;
}

/* Left-hand pages mirror the sheet, exactly as the spread does in Figma */
.page-left::before { transform: scaleX(-1); }

/* Gutter shading leans toward the fold, which swaps sides across the spine.
   Softened well below the Figma values: the white inset highlight read as a
   hard grey strip down the centre, and a tight dark edge beside it made the
   fold look printed on rather than folded. Wide, low-alpha and warm instead. */
/* The crease sits on each page's gutter side, which is the middle of the
   book: that is the LEFT edge of a right-hand page (positive x offset) and
   the RIGHT edge of a left-hand page (negative). */
.page::after {
  content: '';
  position: absolute;
  inset: 0;
  pointer-events: none;
  box-shadow: inset 20px 0 24px -6px rgba(30, 8, 8, 0.14);
}

.page-left::after {
  box-shadow: inset -20px 0 24px -6px rgba(30, 8, 8, 0.14);
}

/* ---------- The cover ---------- */

.cover {
  background: var(--cover-red);
  border-radius: 13.5px;
  box-shadow: 2px 4px 30px rgba(0, 0, 0, 0.25);
}

/* The board supplies its own grain and emboss, so switch off the shared
   page texture and gutter shading. */
.cover::before,
.cover::after { display: none; }

/* The binding: a 55px band down the left with its own inner shadow */
.cover-spine {
  position: absolute;
  top: 0;
  bottom: 0;
  left: 0;
  width: 55px;
  background: var(--cover-red);
  box-shadow:
    inset 0 4.49px 4.49px rgba(0, 0, 0, 0.25),
    inset -11.23px 0 11.23px rgba(0, 0, 0, 0.15);
}

.cover-grain {
  position: absolute;
  inset: 0;
  background: url('assets/paper-texture.jpg') center / cover no-repeat;
  mix-blend-mode: multiply;
  pointer-events: none;
}

.cover-emboss {
  position: absolute;
  inset: 0;
  border-radius: inherit;
  pointer-events: none;
  box-shadow:
    inset 4.49px 4.49px 4.49px rgba(255, 255, 255, 0.25),
    inset -11.23px 0 11.23px rgba(0, 0, 0, 0.15);
}

/* Column sits 12.8px right of centre, clearing the binding */
/* The stack sits 50.5px down - Frame 7 at -2.1 plus Frame 1 at 52.6 - which
   with a 489px block on a 593px board leaves 53px underneath, so it reads as
   centred. Its own centre is 13.9px right of the board's, clearing the spine. */
.cover-inner {
  position: absolute;
  /* The design centres the stack on the cover, offset a touch right of the
     spine's midline. Centring rather than a measured top so the block stays
     put if a line count changes. */
  left: calc(50% + 13.9px);
  top: 50%;
  width: 401px;
  transform: translate(-50%, -50%);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 24px;
  text-align: center;
  color: var(--card);
}

/* Above the title in the design, set as a wide-tracked line of small caps
   rather than the display face - it reads as the strapline on a jacket. */
.cover-subtitle {
  width: 271px;
  font-family: var(--body);
  font-weight: 700;
  font-size: 14px;
  line-height: 1.7;
  letter-spacing: 1.26px;
  text-transform: uppercase;
}

.cover-title {
  width: 371px;
  font-size: 74px;
  line-height: 1;
  letter-spacing: -1.48px;
}

.cover-cat {
  width: 146px;
  height: 131px;
  margin: 15.7px 0 19.1px;
}

.cover-author {
  font-family: var(--body);
  font-weight: 700;
  font-size: 28px;
  line-height: 1.4;
  /* The design reports -2.24px, but that renders 12px narrower than its
     own frame; -1.3px puts the word back on the design's 231px. */
  letter-spacing: -1.3px;
  text-transform: uppercase;
}

/* ---------- Left page: the chapter copy ---------- */

/* Each element sits at its own y from the design: the body copy starts at a
   fixed 330px whether the headline runs to two lines or three. */
.copy { position: absolute; inset: 0; }

.eyebrow {
  position: absolute;
  left: 56px;
  top: 83px;
  font-family: var(--display);
  font-weight: 700;
  font-size: 14px;
  line-height: 1.4;
  letter-spacing: 1.4px;
  text-transform: uppercase;
}

.headline {
  position: absolute;
  left: 56px;
  top: 139px;
  width: 480px;
  font-size: 54px;
  line-height: 1.05;
}

.body-text {
  position: absolute;
  left: 56px;
  top: 330px;
  width: 412px;
  font-weight: 600;
  font-size: 16px;
  line-height: 1.6;
}

/* Set as verse. Each line is its own block, because text-indent only applies
   to the first line of a block - with <br> the hang landed on lines two
   onward instead of on wrapped continuations. Per line, a wrap now hangs and
   reads as one line running on rather than two lines of the poem. */
.verse { line-height: 1.75; }

.verse span {
  display: block;
  padding-left: 1.1em;
  text-indent: -1.1em;
}

/* ---------- 9:30 AM - the timer ---------- */

/* The app window, sized to the design and clipping its own contents: the
   task list is meant to run off the bottom edge, and the same clip is what
   hides the cats above the card until they drop in. */
.timer-card {
  position: absolute;
  left: 47px;
  top: 56px;
  width: 446px;
  height: 481px;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 32px;
  padding: 129px 0 0;
  background: var(--card);
  border: 4px solid rgba(0, 0, 0, 0.05);
  border-radius: 22px;
  box-shadow: inset 0 0 10px rgba(0, 0, 0, 0.08);
  overflow: hidden;
}

.timer-head {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 16px;
}

/* Line boxes are pinned to the design's frame heights rather than left to
   the page's 1.6 leading - otherwise every gap runs a few pixels long and
   the stack drifts about 25px low by the time it reaches the task list. */
.timer-greeting {
  font-weight: 700;
  font-size: 19.7px;
  line-height: normal;
  letter-spacing: -1.18px;
  white-space: nowrap;
}

/* The three modes ride in a single outlined capsule; only the live one
   carries a fill, its own darker rim, and the texture shadow. */
.mode-pill {
  display: flex;
  align-items: center;
  gap: 3px;
  height: 34px;
  padding: 0 5px;
  background: var(--card);
  border: 2px solid var(--ink);
  border-radius: 999px;
  box-shadow: 3px 3px 0 rgba(0, 0, 0, 0.1);
}

.mode {
  display: inline-flex;
  align-items: center;
  height: 23px;
  padding: 0 11px;
  border: 2px solid transparent;
  border-radius: 999px;
  font-family: var(--display);
  font-weight: 700;
  font-size: 12px;
  line-height: 1;
}

.mode.is-on {
  background: var(--sun);
  border-color: #ddbc00;
  box-shadow:
    2px 4px 10px rgba(0, 0, 0, 0.1),
    inset 4.49px 4.49px 4.49px rgba(255, 255, 255, 0.25),
    inset 2px 0 5px rgba(0, 0, 0, 0.15);
}

.readout {
  display: flex;
  align-items: center;
  justify-content: center;
  height: 72px;
  padding: 8px 19px;
  background: #fff;
  border: 2px solid var(--ink);
  border-radius: 999px;
  box-shadow: 3px 3px 0 rgba(0, 0, 0, 0.1);
  font-family: var(--display);
  font-weight: 700;
  font-size: 61px;
  line-height: 1.1;
}

/* A white puck sits behind the button carrying the hard offset shadow, so
   the red keeps its own soft one instead of doubling up. */
.start-wrap {
  display: inline-flex;
  background: #fff;
  border-radius: 999px;
  box-shadow: 2px 2px 0 rgba(0, 0, 0, 0.8);
}

.start {
  display: inline-flex;
  align-items: center;
  height: 37px;
  padding: 0 26px;
  background: var(--red);
  border: 2px solid #ad0101;
  border-radius: 999px;
  color: #fff;
  font-family: var(--display);
  font-weight: 700;
  font-size: 16px;
  line-height: 1.2;
  box-shadow:
    inset 4.49px 4.49px 4.49px rgba(255, 255, 255, 0.25),
    inset 2px 0 5px rgba(0, 0, 0, 0.15);
  filter: drop-shadow(2px 4px 5px rgba(0, 0, 0, 0.1));
}

.task-card {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 9.8px;
  width: 326px;
  height: 358.5px;
  flex: none;
  padding: 14.8px 19.7px;
  background: #fff6e2;
  border: 2px solid #c6bdab;
  border-radius: 4px;
  box-shadow: 5px 5px 0 rgba(0, 0, 0, 0.8);
}

.task-title {
  font-family: var(--display);
  font-weight: 700;
  font-size: 16px;
  line-height: 1.12;
  text-transform: uppercase;
}

.task-add {
  display: flex;
  align-items: center;
  gap: 7.9px;
  width: 286.6px;
  padding: 5.9px;
  border-bottom: 1px solid #857e7e;
  font-weight: 700;
  font-size: 11.8px;
  color: #857e7e;
}

/* --- the cats, and the drop they arrive with --- */

/* Hidden until the drop is armed. The card can be on screen for up to 620ms
   before that happens - a page turn waits for the leaf to land - and in that
   window the cats sat at their resting places in plain sight, then vanished
   to the spawn line and fell, which reads as a glitch rather than an entrance.
   Reduced motion skips the drop altogether and never adds the class, so the
   media query below leaves them where they are. */
.drop-cat {
  position: absolute;
  width: 58.5px;
  visibility: hidden;
}

.cats-drop .drop-cat { visibility: visible; }

@media (prefers-reduced-motion: reduce) {
  .drop-cat { visibility: visible; }
}

.drop-cat img {
  display: block;
  width: 100%;
  height: auto;
  transform: rotate(var(--rot, 0deg));
}

/* Timings taken from the app rather than tuned by eye. Its physics world
   runs Rapier at gravity 9.81 with ZOOM 50 px per world unit, so cats fall at
   490.5 px/s^2, and a cat image is 1.5 units = 75px. The book draws the same
   cat at 58.5px, which is 0.78 of that, so the whole scene is that fall
   replayed at 0.78 scale: g = 382.6 px/s^2 and the spawn line - `worldHeight
   / 2 + 2` in Pomodoro's logged-out preview rain, two units above the top
   edge - is 78px above the card. Durations are then t = sqrt(2h/g), times 1.4 for the bounce.

   Delays come from the same effect: `4000 / CAT_NAMES.length` staggers the
   whole intro over four seconds, one cat at a time, 333ms apart. The order is
   CAT_NAMES order, which for the four cats on this page is george, tuxedo,
   bean, sweetie.

   --rot is held for the entire fall. The intro cats are previews, and a
   preview's collider is a sensor: it touches nothing on the way down, so no
   torque ever reaches it and the angle it spawns with is the angle it keeps. */
.drop-george  { left: 27.7px;  top: 299.2px; --rot: -11.77deg; --fall: 377.2px; --ms: 1966ms; --delay: 0ms; }
.drop-tux     { left: 45.7px;  top: 89.9px;  --rot: -22.21deg; --fall: 167.9px; --ms: 1312ms;  --delay: 333ms; }
.drop-bean    { left: 275px;   top: 23px;    --rot: 0deg;      --fall: 101px;   --ms: 1018ms;  --delay: 667ms; }
.drop-sweetie { left: 337.2px; top: 334.8px; --rot: 13.12deg;  --fall: 412.8px; --ms: 2057ms; --delay: 1000ms; }

/* Free fall under constant gravity, which is y = t^2 exactly. A cubic
   Bezier reproduces that exactly with control points (1/3, 0) and
   (2/3, 1/3) - no easing curve needs to be guessed at.
   Every cat starts on the same line above the card, the way the app spawns
   them all on one line above the viewport; the card's own clip keeps them
   out of sight until they enter frame, so no fade is needed. */
/* Fall, then one small bounce. A coefficient of restitution e rebounds to
   e-squared of the drop and takes e times the fall time to get there, so at
   e = 0.2 the hop is 4% of the fall and the whole thing runs 1.4x as long -
   which is why --ms below is the free-fall time times 1.4. Those ratios do
   not depend on the height, so the keyframe stops are the same for every cat:
   the landing at 1/1.4 = 71.43%, the apex at 1.2/1.4 = 85.71%.

   Each leg carries its own easing. Falling is y = t^2 exactly, which is the
   cubic Bezier (1/3, 0) (2/3, 1/3); rising against gravity is its mirror,
   (1/3, 2/3) (2/3, 1). Neither is guessed at. */
@keyframes cat-fall {
  0% {
    transform: translateY(calc(var(--fall) * -1));
    animation-timing-function: cubic-bezier(0.333, 0, 0.667, 0.333);
  }
  71.43% {
    transform: translateY(0);
    animation-timing-function: cubic-bezier(0.333, 0.667, 0.667, 1);
  }
  85.71% {
    transform: translateY(calc(var(--fall) * -0.04));
    animation-timing-function: cubic-bezier(0.333, 0, 0.667, 0.333);
  }
  100% { transform: translateY(0); }
}

.cats-drop .drop-cat {
  animation: cat-fall var(--ms) cubic-bezier(0.333, 0, 0.667, 0.333) var(--delay) both;
}

/* ---------- 11:30 AM - the canvas ---------- */

.canvas-board {
  position: absolute;
  left: 44px;
  top: 67px;
  width: 453px;
  height: 459px;
  background: var(--card);
  border: 4px solid rgba(30, 8, 8, 0.05);
  border-radius: 22px;
  overflow: hidden;
  box-shadow: inset 0 0 10px rgba(30, 8, 8, 0.08);
}

.dot-grid {
  position: absolute;
  inset: -30px;
  background-image: radial-gradient(rgba(30, 8, 8, 0.1) 1.4px, transparent 1.4px);
  background-size: 22px 22px;
}

.note {
  position: absolute;
  width: 184px;
  padding: 16px 18px 18px;
  border: 2px solid;
  box-shadow: 0 2px 4px rgba(30, 8, 8, 0.1);
}

.note-lime {
  left: 22px;
  top: 151px;
  background: var(--lime);
  border-color: #a2be7f;
  transform: rotate(-3deg);
}

.note-sky {
  left: 249px;
  top: 83px;
  background: var(--sky-a);
  border-color: #98c9da;
  transform: rotate(4deg);
}

.note .pin {
  position: absolute;
  left: 144px;
  top: -15px;
  width: 26px;
  height: 31px;
}

.note-title {
  display: flex;
  align-items: center;
  gap: 3px;
  font-family: var(--display);
  font-weight: 700;
  font-size: 13px;
  line-height: 1.4;
  letter-spacing: 1.04px;
  text-transform: uppercase;
}

.note-cat { width: 17px; height: 16px; }

.note-items {
  list-style: none;
  margin: 6px 0 0;
  padding: 0;
}

.note-items li {
  position: relative;
  padding: 2px 0 2px 22px;
  font-weight: 600;
  font-size: 14px;
  line-height: 1.6;
}

.note-items li::before {
  content: '';
  position: absolute;
  left: 0;
  top: 7px;
  width: 12px;
  height: 12px;
  border-radius: 4px;
  border: 1px solid rgba(60, 60, 67, 0.6);
  background: rgba(120, 120, 120, 0.2);
}

.note-items li.is-done { opacity: 0.65; }

.note-items li.is-done::before {
  background: var(--ink);
  border-color: var(--ink);
}

.note-items li.is-done::after {
  content: '';
  position: absolute;
  left: 3px;
  top: 10px;
  width: 6px;
  height: 3px;
  border-left: 1.6px solid var(--card);
  border-bottom: 1.6px solid var(--card);
  transform: rotate(-45deg);
}

.omnibar {
  position: absolute;
  left: 14px;
  bottom: 16px;
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 8px 16px 8px 8px;
  background: #fff;
  border: 1px solid #d9d9d9;
  border-radius: 999px;
}

.omnikey {
  font-family: var(--display);
  font-weight: 700;
  font-size: 12px;
  line-height: 1.4;
  background: var(--sun);
  border-radius: 999px;
  padding: 6px 8px;
  box-shadow: inset 0 4px 4px rgba(255, 255, 255, 0.25);
}

.omnitext { font-weight: 600; font-size: 14px; line-height: 1.6; }

.board-cat {
  position: absolute;
  left: 280px;
  bottom: 0;
  width: 92px;
  height: 83px;
  transform: rotate(16.1deg);
  filter: drop-shadow(1.2px 2.4px 3px rgba(30, 8, 8, 0.1));
}

/* ---------- 3:00 PM - payday ---------- */

.gacha-stage { position: absolute; inset: 0; }

/* Scaled so the body sits where the design puts it, which pushes the dome
   and lid above the page edge - the page's own overflow does the cropping. */
.gacha {
  position: absolute;
  left: 112px;
  top: -228px;
  width: 312px;
  height: auto;
}

/* The ball is 89 of the asset's 173px - the rest is its soft shadow. At
   172px wide it reads 88px across, a shade larger than the ~75px capsules
   behind the dome glass, so it sits nearer the reader. Centred on the
   dispenser door (page x 267) with its top tucked just under the lid. */
.gacha-capsule {
  position: absolute;
  left: 188px;
  top: 191px;
  width: 172px;
  height: auto;
  /* The asset ships with a grey drop shadow baked in, which read as heavy
     against the paper; it has been erased from the PNG so the shadow can
     live here instead - a whisper of contact shade under the ball, then a
     warm halo in the machine's own yellow to say "freshly dispensed". */
  filter:
    drop-shadow(0 2px 3px rgba(30, 8, 8, 0.1))
    drop-shadow(0 0 10px rgba(255, 219, 112, 0.85))
    drop-shadow(0 0 24px rgba(251, 213, 0, 0.3));
}

.pay-cat { position: absolute; filter: drop-shadow(1.5px 3px 4px rgba(30, 8, 8, 0.14)); }
.pay-cat-1 { left: 88px; top: 365px; width: 112px; height: 101px; transform: rotate(-4deg); }
.pay-cat-2 { left: 222px; top: 462px; width: 110px; height: 99px; transform: rotate(7deg); }
.pay-cat-3 { left: 355px; top: 388px; width: 110px; height: 100px; transform: rotate(-9deg); }

.payout-chip {
  position: absolute;
  left: 280px;
  top: 370px;
  font-family: var(--display);
  font-weight: 700;
  font-size: 13px;
  line-height: 1.4;
  background: var(--sun);
  border-radius: 999px;
  padding: 5px 12px;
  transform: rotate(-7deg);
  box-shadow: 0 2px 4px rgba(30, 8, 8, 0.12), inset 0 4px 4px rgba(255, 255, 255, 0.3);
}

/* ---------- 6:30 PM - the ledger ---------- */

.stats-stage { position: absolute; inset: 0; }

/* Card corners really are near-square in the design (2.4px on a 350px card);
   what softens it is the pair of shadows, one thrown and one inset. */
.chart-card {
  position: absolute;
  left: 45px;
  top: 80px;
  width: 347px;
  height: 262px;
  background: var(--card);
  border: 1px solid rgba(30, 8, 8, 0.1);
  border-radius: 2.4px;
  box-shadow:
    0 0 4.9px rgba(0, 0, 0, 0.1),
    inset 1.2px 1.2px 4.9px rgba(0, 0, 0, 0.1);
  padding: 21.9px 23.2px 19.5px;
  overflow: hidden;
}

.card-label {
  font-weight: 700;
  font-size: 19.5px;
  line-height: 1.6;
  letter-spacing: -0.78px;
}

.chart {
  display: flex;
  align-items: flex-end;
  gap: 9.8px;
  height: 146.3px;
  margin-top: 13.4px;
  border-bottom: 1px solid rgba(159, 159, 159, 0.2);
}

/* Each bar is lit from the top left and throws a soft shadow down and to the
   right - that pair is what gives the deck its depth. The two spent days keep
   the inset but lose the throw, so they read as empty slots. */
.bar {
  flex: 1;
  height: var(--h);
  background: var(--sun);
  border-radius: 8.5px 8.5px 0 0;
  box-shadow:
    2.4px 4.9px 12.2px rgba(0, 0, 0, 0.1),
    inset 5.5px 5.5px 5.5px rgba(255, 255, 255, 0.25);
}

.bar-today { background: var(--red); }

.bar-soft {
  background: var(--card);
  box-shadow: inset 2.4px 0 6.1px rgba(0, 0, 0, 0.08);
}

.chart-days { display: flex; gap: 9.8px; margin-top: 7.3px; }

.chart-days span {
  flex: 1;
  text-align: center;
  font-weight: 700;
  font-size: 13.4px;
  line-height: 1.4;
  color: #646464;
  opacity: 0.7;
}

/* The outline is a soft sage that belongs to the lime rather than the
   near-black ink the rest of the page draws with - that is what makes the
   chip read as a sticker instead of an outlined button. The lettering does
   stay ink: get_design_context reports #264500 for it, but the frame render
   has #1e0808 and no trace of the green, so the render wins. Tilt is the
   design's own 6.25 degrees. */
.streak-chip {
  position: absolute;
  left: 307px;
  top: 78px;
  display: inline-flex;
  align-items: center;
  gap: 5.6px;
  height: 33.2px;
  padding: 0 15.7px;
  font-family: var(--display);
  font-weight: 700;
  font-size: 15.7px;
  line-height: 1;
  color: var(--ink);
  background: var(--lime);
  border: 2.25px solid #a2be7f;
  border-radius: 999px;
  white-space: nowrap;
  transform: rotate(-6.25deg);
  box-shadow:
    0 2.25px 11.2px rgba(0, 0, 0, 0.08),
    inset 5px 5px 5px rgba(255, 255, 255, 0.25);
}

/* Three toes and a pad, ellipse for ellipse out of the design's own export. */
.paw { flex: none; width: 15.7px; height: 14.6px; }

.cal-chip {
  position: absolute;
  left: 410px;
  top: 138px;
  width: 92px;
  text-align: center;
  background: var(--card);
  border: 1px solid rgba(30, 8, 8, 0.07);
  border-radius: 10px;
  box-shadow: 0 4px 14px rgba(30, 8, 8, 0.12);
  padding: 7px 7px 10px;
}

.cal-day {
  font-family: var(--display);
  font-weight: 700;
  font-size: 12px;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--card);
  background: var(--red);
  border-radius: 5px;
  padding: 2px 0;
}

.cal-num { font-family: var(--display); font-weight: 700; font-size: 30px; line-height: 1.2; }
.cal-note { font-weight: 600; font-size: 11px; color: rgba(30, 8, 8, 0.65); }

.stats-cat {
  position: absolute;
  left: 46px;
  top: 455px;
  width: 98px;
  height: 89px;
}

.speech {
  position: absolute;
  left: 128px;
  top: 412px;
  width: 310px;
  font-weight: 600;
  font-size: 15px;
  line-height: 1.5;
  background: #f6e6cd;
  border-radius: 18px;
  padding: 18px 22px;
}

/* ---------- 11:59 PM - the night shift ---------- */

.page-night { background: var(--night-b); }
.page-night::before { display: none; }

/* Right-hand page, so its crease is on the left too */
.page-night::after {
  box-shadow: inset 22px 0 26px -6px rgba(0, 0, 0, 0.36);
}

.night-stage { position: absolute; inset: 0; overflow: hidden; }

.night-sky {
  position: absolute;
  inset: 0;
  background:
    radial-gradient(52% 40% at 50% 96%, rgba(174, 138, 158, 0.5), rgba(174, 138, 158, 0) 70%),
    radial-gradient(120% 92% at 78% 16%, #5b3a4d 0%, #48293b 46%, #331828 100%);
}

.moon {
  position: absolute;
  left: 390px;
  top: 38px;
  width: 110px;
  height: 110px;
  border-radius: 50%;
  background: #f4e8d9;
  box-shadow:
    0 0 48px rgba(244, 232, 217, 0.42),
    inset -14px -9px 0 rgba(30, 8, 8, 0.06);
}

/* Size drives everything else: the glow scales with the cat so a 7px one is
   a pinprick rather than all halo, and --o dims the small ones so they read
   as further off. Each breathes on its own clock, which is what stops them
   pulsing as one. */
.star-cat {
  position: absolute;
  width: var(--s, 20px);
  height: auto;
  filter: drop-shadow(0 0 calc(var(--s, 20px) * 0.42) rgba(255, 255, 255, 0.85));
  animation: star-breathe var(--t, 6s) ease-in-out var(--d, 0s) infinite alternate;
}

/* A shallow dip. At half opacity the 6px ones dropped out of the sky
   altogether between beats, which the design - where they are static -
   never does. */
@keyframes star-breathe {
  from { opacity: calc(var(--o, 1) * 0.72); }
  to   { opacity: var(--o, 1); }
}

/* Sizes and places are the design's own eight Blanco instances (page 8):
   one big one on a comet at 57.6px, then 23, 17.7, 12.8, 8.8, 8.8, 5.7, 5.7.
   Almost everything is small - that hierarchy is what makes it read as a
   sky rather than as a scattering of cats. */
.sc-1 { left: 45px;  top: 63px;  --s: 13px; --t: 6.4s; --d: 0s; }
.sc-2 { left: 523px; top: 351px; --s: 9px;  --o: 0.94; --t: 7.1s; --d: 0.8s; }
.sc-3 { left: 370px; top: 30px;  --s: 9px;  --o: 0.94; --t: 5.2s; --d: 1.7s; }
.sc-4 { left: 322px; top: 129px; --s: 6px;  --o: 0.88; --t: 7.6s; --d: 1.2s; }
.sc-5 { left: 217px; top: 14px;  --s: 6px;  --o: 0.88; --t: 4.6s; --d: 2.9s; }

.comet { position: absolute; display: block; }

/* Head size in one place, with the tail lengths below cut to match. */
.comet img {
  display: block;
  width: var(--cs, 20px);
  height: auto;
  filter: drop-shadow(0 0 calc(var(--cs, 20px) * 0.42) rgba(255, 255, 255, 0.95));
}

.comet::before {
  content: '';
  position: absolute;
  left: 72%;
  bottom: 52%;
  width: 90px;
  height: 7px;
  border-radius: 999px;
  background: linear-gradient(to right, rgba(255, 255, 255, 0.85), rgba(255, 255, 255, 0));
  transform-origin: left center;
  transform: rotate(-19deg);
  filter: blur(1.5px);
}

/* The three that carry tails are simply the three largest Blancos. Tail
   thickness scales with the head - a single 7px bar left the big one's
   streak looking like a thread next to a 58px cat. */
.comet-1 { left: 22px;  top: 205px; --cs: 58px; }
.comet-1::before { width: 125px; height: 12px; }
.comet-2 { left: 157px; top: 101px; --cs: 18px; }
.comet-2::before { width: 80px; height: 6px; }
.comet-3 { left: 466px; top: 206px; --cs: 23px; }
.comet-3::before { width: 62px; height: 8px; }

.night-label {
  position: absolute;
  left: 0;
  right: 0;
  top: 236px;
  text-align: center;
  font-weight: 600;
  font-size: 15px;
  color: rgba(255, 254, 252, 0.92);
}

/* A plain rounded rectangle on the same paper as the pages - it was a
   clip-path trapezoid, which is the tapered silhouette you get if you read
   the design's shape as its own outline rather than as a rectangle with the
   cat's ears cutting into it. Measured off the design: x 872..1107 and top
   edge at 427 of the 1440 x 900 frame. */
.popout {
  position: absolute;
  left: 152px;
  top: 273px;
  width: 236px;
  height: 96px;
  display: flex;
  align-items: center;
  justify-content: center;
  background: #f7ebda;
  border-radius: 16px;
  overflow: hidden;
  /* Keeps the grain multiplying against the card's own cream rather than
     against the night panel underneath it. */
  isolation: isolate;
  box-shadow: 0 0 40px rgba(248, 232, 204, 0.28);
}

.popout::before {
  content: '';
  position: absolute;
  inset: 0;
  background: url('assets/paper-texture.jpg') center / cover no-repeat;
  mix-blend-mode: multiply;
  opacity: 0.55;
  pointer-events: none;
}

.popout-time {
  position: relative;
  font-family: var(--display);
  font-weight: 700;
  font-size: 39px;
  line-height: 1;
  color: #4a3325;
}

/* Sits low enough that the page crops the chin, as the design does */
.night-cat {
  position: absolute;
  left: 52px;
  bottom: -95px;
  width: 440px;
  height: 398px;
}

/* ---------- The closing screen ----------

   Same camera stop as the night: nothing pans. The book steps back and
   fades, the wheel turns, and this rises in its place. */
.cta {
  position: fixed;
  left: 0;
  right: 0;
  top: 38vh;
  z-index: 3;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 34px;
  text-align: center;
  opacity: 0;
  transform: translateY(26px);
  pointer-events: none;
  transition: opacity 700ms ease-out 900ms, transform 900ms cubic-bezier(0.2, 0.7, 0.3, 1) 900ms,
    top 620ms cubic-bezier(0.3, 0.08, 0.2, 1);
}

/* The panel is a fixed 380px tall, so whether it reaches the button depends
   entirely on the window: on a tall screen there is room to spare, on a short
   one there is none. The step up is therefore exactly the shortfall - the
   panel's top (100vh - 380) has to clear the button's bottom (38vh + 201) by
   26px, which is short by (607px - 62vh) - and nothing at all when that goes
   negative. It moves on the panel's own curve and duration so the two read as
   one movement, and uses `top` rather than the transform, which is spoken for
   by the entry. The floor keeps it clear of the navbar on a very short
   window, where the panel's own cap takes over. */
body.is-footer-open .cta { top: max(96px, calc(38vh - max(0px, 607px - 62vh))); }

body[data-chapter='6'] .cta {
  opacity: 1;
  transform: translateY(0);
  pointer-events: auto;
}

/* Arriving, the copy waits for the wheel to turn before it rises. Leaving,
   that same delay left it sitting over the last spread as the book came
   back, so going the other way it clears immediately. */
body:not([data-chapter='6']) .cta {
  transition-duration: 240ms, 240ms;
  transition-delay: 0ms, 0ms;
}

.cta-title {
  font-size: 48px;
  line-height: 1.15;
  letter-spacing: -0.6px;
  color: var(--ink);
}

.cta-btn {
  display: inline-flex;
  align-items: center;
  height: 57px;
  padding: 0 34px;
  background: var(--red);
  border-radius: 999px;
  color: #fff;
  font-family: var(--display);
  font-weight: 700;
  font-size: 21px;
  text-decoration: none;
  box-shadow: 0 4px 14px rgba(173, 1, 1, 0.28);
  transition: transform 180ms ease, box-shadow 180ms ease;
}

.cta-btn:hover { transform: translateY(-2px); box-shadow: 0 7px 20px rgba(173, 1, 1, 0.34); }

/* The book bows out rather than vanishing: it keeps its place in the layout
   so the stage does not reflow, and the hint goes with it. */
body[data-chapter='6'] .book-slot {
  /* Covers the 2.9s window where the intro is still running rather than
     merely filling: an animation in flight outranks these declarations too. */
  animation: none;
  opacity: 0;
  transform: scale(0.94) translateY(18px);
  transition: opacity 700ms ease-in, transform 1100ms cubic-bezier(0.4, 0, 0.2, 1);
}

body[data-chapter='6'] .nav-btn,
body[data-chapter='6'] .hint { opacity: 0; pointer-events: none; }

.is-ready .book-slot { transition: opacity 600ms ease-out, transform 900ms ease-out; }
.is-ready .nav-btn,
.is-ready .hint { transition: opacity 500ms ease-out; }

/* ---------- Page-turn controls ---------- */

.nav-btn {
  flex: 0 0 auto;
  width: 48px;
  height: 48px;
  padding: 12px;
  display: grid;
  place-items: center;
  color: var(--ink);
  background: rgba(255, 254, 252, 0.7);
  border: 0;
  border-radius: 999px;
  cursor: pointer;
  transition: background 0.16s ease, transform 0.16s ease, opacity 0.2s ease;
}

.nav-icon { display: block; width: 24px; height: 24px; }

/* Back points the other way */
.nav-prev .nav-icon { transform: scaleX(-1); }

.nav-btn:hover { background: rgba(255, 254, 252, 0.92); transform: scale(1.06); }

.nav-btn[disabled] {
  opacity: 0.32;
  cursor: default;
  transform: none;
}

/* On a phone the arrows would eat a quarter of the width; tapping the page
   and the bookmarks carries the interaction instead. */
@media (max-width: 700px) {
  .nav-btn { display: none; }
}

/* ---------- Cover intro ----------

   Timings taken from the designed 10.5s timeline (node 380:3323): the glows
   rise first, the sun follows with its overshoot, the book swings up from
   -90deg and settles at 2s, and the clouds drift down from 2x scale across
   the whole run. Only plays on a first load that lands on the cover. */

.intro .glow { animation: intro-glow 2000ms cubic-bezier(0.479, -0.025, 0.298, 1.21) both; }
.intro .glow-2 { animation-delay: 500ms; }
.intro .glow-3 { animation-delay: 900ms; }

@keyframes intro-glow {
  0% { opacity: 0; transform: translateY(200px) scale(1); }
  47% { opacity: 1; }
  100% { opacity: 1; transform: translateY(0) scale(1.1); }
}

/* Straight up the middle, no scale-from-nothing - that read as swinging in
   from the side. Three independent properties are in play at once:
   `transform` rises, `rotate` turns the rays, `translate` does the idle
   drift, so none of them overwrite each other. */
@keyframes intro-sun {
  0% { opacity: 0; transform: translateY(52vh); }
  55% { opacity: 1; }
  100% { opacity: 1; transform: translateY(0); }
}

@keyframes sun-spin {
  from { rotate: 0deg; }
  to { rotate: 360deg; }
}

@keyframes sun-drift {
  0%, 100% { translate: 0 0; }
  50% { translate: 0 1.8vh; }
}

.sun {
  animation:
    sun-spin 16s linear infinite,
    sun-drift 7s ease-in-out infinite;
}

/* Follows the book up rather than leading it */
.intro .sun {
  animation:
    intro-sun 3000ms cubic-bezier(0.22, 0.68, 0.3, 1) 950ms both,
    sun-spin 16s linear 950ms infinite,
    sun-drift 7s ease-in-out 3950ms infinite;
}

/* Rises from below with just a touch of rotation, arriving after the clouds
   have begun to part.

   `backwards`, not `both`, unlike its siblings here. Those fill forwards
   because their end states - drifted clouds, glows at 1.1 - ARE the resting
   scene. This one's end state is simply the book's default, and a forward
   fill on opacity/transform outranks every declaration for the life of the
   page: with body.intro never removed, it silently beat the closing screen's
   `opacity: 0` and left the whole book sitting behind the CTA. Backwards
   still holds the start state through the 300ms delay, which is all the fill
   was ever for. */
.intro .book-slot { animation: intro-book 2600ms cubic-bezier(0.24, 0.72, 0.28, 1) 300ms backwards; }

@keyframes intro-book {
  0% { opacity: 0; transform: translateY(78%) rotate(-6deg); }
  20% { opacity: 0.9; }
  100% { opacity: 1; transform: translateY(0) rotate(0deg); }
}

.intro .cover-cat { animation: intro-fade 800ms cubic-bezier(0.25, 1, 0.5, 1) 1100ms both; }

@keyframes intro-fade {
  from { opacity: 0; }
  to { opacity: 1; }
}

/* The background clouds just drift */
.intro .cloud { animation: intro-cloud 13000ms linear both; }
.intro .cl-1 { --drift: 234px; animation-delay: 200ms; }
.intro .cl-2 { --drift: 115px; }
.intro .cl-3 { --drift: -190px; animation-delay: 300ms; }


@keyframes intro-cloud {
  0% { opacity: 0.8; transform: translateX(calc(var(--drift, 200px) * -0.32)) scale(1.7); }
  68% { opacity: 0.6; transform: translateX(var(--drift, 200px)) scale(1); }
  100% { opacity: 0.6; transform: translateX(var(--drift, 200px)) scale(1); }
}

/* The two big ones sit over the middle while the book rises through them,
   then pan out to their own sides. The parting overlaps the book's travel
   (300-2900ms) rather than waiting for it to finish. */
.intro .cl-left,
.intro .cl-right { animation: intro-curtain 3400ms cubic-bezier(0.36, 0.06, 0.24, 1) both; }

.intro .cl-left { --part-x: 30vw; --part-y: -28vh; }
.intro .cl-right { --part-x: -28vw; --part-y: -24vh; }

@keyframes intro-curtain {
  0% { opacity: 0.95; transform: translate(var(--part-x), var(--part-y)) scale(1.5); }
  18% { opacity: 0.95; transform: translate(var(--part-x), var(--part-y)) scale(1.5); }
  100% { opacity: 0.9; transform: translate(0, 0) scale(1); }
}

/* Directly under the book: comes straight up from the bottom middle, a beat
   behind the book itself */
.intro .cl-under { animation: intro-cloud-up 3000ms cubic-bezier(0.24, 0.72, 0.28, 1) 800ms both; }

@keyframes intro-cloud-up {
  0% { opacity: 0.6; transform: translateY(46vh) scale(1.25); }
  100% { opacity: 0.9; transform: translateY(0) scale(1); }
}

.intro .bird { animation: intro-bird 2500ms cubic-bezier(0.25, 1, 0.5, 1) 500ms both; }
.intro .bird-2 { animation-delay: 1000ms; }
.intro .bird-3 { animation-delay: 700ms; }
.intro .bird-4 { animation-delay: 900ms; }

@keyframes intro-bird {
  0% { opacity: 0; transform: translate(60px, 30px) scale(0.1); }
  38% { opacity: 1; }
  100% { opacity: 0.72; transform: translate(0, 0) scale(1); }
}

/* ---------- The footer, as a panel ----------

   The contents are in chrome.css, shared with the document pages. What is
   here is only what makes it a panel instead of the end of a page: the guide
   is a fixed viewport with no scroll of its own, so rather than sitting after
   the closing screen in the document the footer sits under it. On the last
   chapter its top edge rests along the bottom, and carrying on past the CTA -
   scroll, swipe, arrow, or a click on the edge - raises it. */

/* How much of the panel rests in view on the last chapter. On body so the
   hub link in the same corner can measure itself against it. */
body { --footer-lip: 38px; }

.site-footer {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  /* Above .back-link and the page chrome. The navbar sits at the top and the
     panel is capped short of it. */
  z-index: 6;
  border-radius: 18px 18px 0 0;
  box-shadow: 0 -4px 24px rgba(30, 8, 8, 0.11);
  transform: translateY(100%);
  transition: transform 620ms cubic-bezier(0.3, 0.08, 0.2, 1);
}

body[data-chapter='6'] .site-footer { transform: translateY(calc(100% - var(--footer-lip))); }
body[data-chapter='6'].is-footer-open .site-footer { transform: translateY(0); }

/* Unlabelled: a chevron on the bottom edge of the screen already reads as
   "there is more below", and a caption there competed with the closing line
   right above it. The name lives in aria-label for anyone who can't see it. */
.footer-toggle {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  height: var(--footer-lip);
  padding: 0;
  background: none;
  border: 0;
  color: rgba(30, 8, 8, 0.5);
  cursor: pointer;
  transition: color 0.16s ease;
}

.footer-toggle:hover { color: var(--ink); }

/* Points down to dismiss once the panel is up */
.footer-chevron { display: block; transform: rotate(180deg); transition: transform 320ms ease; }
.is-footer-open .footer-chevron { transform: rotate(0deg); }

/* The chevron already occupies 38px above this; the rest is the gap between
   it and the first column head, which wants to be generous - it is the only
   thing separating a control from the content it revealed. Capped, as a
   floor of last resort, so a very short window scrolls the panel rather than
   letting it run off the top. */
.footer-body {
  padding-top: 26px;
  max-height: calc(100vh - 130px);
  overflow-y: auto;
}

@media (max-width: 700px) {
  .footer-body { max-height: calc(100vh - 90px); }
}

/* ---------- Reduced motion ---------- */

@media (prefers-reduced-motion: reduce) {
  .site-footer,
  .footer-chevron { transition: none; }

  .is-ready .book,
  .is-ready .leaf { transition: none; }


  .book.is-settling { animation: soft-fade 200ms ease; }

  @keyframes soft-fade {
    from { opacity: 0.25; }
    to { opacity: 1; }
  }
}
