/* Ash, embers and a slow burn on a boss leaf (Fable's book-ash plan and plan v3, 2026-11-14; views/book.mjs,
   public/book-burn.js, public/book-holes.js). Only boss leaves load this file (routes/book.mjs): every boss was cursed,
   so its leaf smoulders; the world chapters only wear with age (app.css).
   - Each page carries its burn stage (burn-0 cover and locked page, burn-1 front half, burn-2 the threshold, burn-3 the
     rest of the back half, burn-out the last page). The char (.pg-burn, tx-burn-N), its ember line (.pg-ember, the
     element's --ember through ember-line-N) and the settled ash (.pg-ash) ride on the page, so they turn with the fold;
     scripts/world/build_book_textures.py keeps the body ink at 7:1 under all of them.
   - Ash and embers fall onto the paper itself (.pg-fall, made by public/book-burn.js: flakes land, lie and crumble; an
     ember glows and dies, or burns a hole), and a few flakes and sparks fall in the margins and on the desk (.book-ash,
     6 <i>, pure CSS, so ash falls without script too); #book[data-burn] (server-rendered, updated on each turn) sets
     how much. Only transform and opacity animate.
   - Holes (.pg-holes > .bh): the embers burn through. A hole shows the next leaf in deep shadow inside a char rim
     (bh-holes, a sprite of six) and never sits on a glyph: the old ones are server slots on the rim and the foot
     (.bh-s1..8), the new ones form where the placer finds empty paper (an ember lands, the paper scorches, the hole
     opens with a glowing rim, the rim cools to char) and are kept per page for the visit (sessionStorage). Only the
     faint outer halo may graze text, at --bh-halo-a, which the build checks against 7:1.
   - Reduced motion and .ash-lite (Save-Data, low memory): no falling ash or embers, the char at its final place, a
     still ember, every hole of the page there at once, cold. */
.book-read.is-boss { --ember: #ff8a2a; }
.book-read.be-fire { --ember: #ff7a1a; } .book-read.be-water { --ember: #9fd4ff; } .book-read.be-nature { --ember: #c6e25a; }
.book-read.be-thunder { --ember: #fff3a0; } .book-read.be-light { --ember: #ffd36a; } .book-read.be-dark { --ember: #b58cff; }

/* the char and its ember: on the page, under the text */
.is-boss .pg-body, .is-boss .pg-folio { z-index: 1; }
.pg-burn { position: absolute; inset: 0; z-index: 0; pointer-events: none; background: url(/public/world/book/tx-burn-1.webp?v=2669ec0144) 0 0 / 100% 100% no-repeat; }
.burn-2 .pg-burn { background-image: url(/public/world/book/tx-burn-2.webp?v=18ef8c25a9); }
.burn-3 .pg-burn { background-image: url(/public/world/book/tx-burn-3.webp?v=d3d90a4747); }
.pg-even .pg-burn { transform: scaleX(-1); }
/* while the reader dwells on a burning page the char creeps in once, ending where the build checked it */
.burn-2:not(.burn-out) .pg-burn, .burn-3:not(.burn-out) .pg-burn { animation: burn-creep 40s cubic-bezier(.2, .6, .3, 1) both; }
.pg-even.burn-2:not(.burn-out) .pg-burn, .pg-even.burn-3:not(.burn-out) .pg-burn { animation-name: burn-creep-m; }
@keyframes burn-creep { from { transform: scale(1.03); } to { transform: none; } }
@keyframes burn-creep-m { from { transform: scale(-1.03, 1.03); } to { transform: scaleX(-1); } }
.pg-ember { position: absolute; inset: 0; opacity: 0; background: var(--ember); -webkit-mask: url(/public/world/book/ember-line-3.webp?v=949645b12c) 0 0 / 100% 100% no-repeat; mask: url(/public/world/book/ember-line-3.webp?v=949645b12c) 0 0 / 100% 100% no-repeat; }
.burn-2 .pg-ember { -webkit-mask-image: url(/public/world/book/ember-line-2.webp?v=911ab5fb69); mask-image: url(/public/world/book/ember-line-2.webp?v=911ab5fb69); animation: ember-catch 1.6s ease-out both, ember-breathe 5s ease-in-out 1.6s infinite; }
.burn-3 .pg-ember { opacity: .8; animation: ember-breathe 5s ease-in-out infinite; }
.burn-out .pg-ember { display: none; } /* the last page: the embers have gone cold */
@keyframes ember-catch { 0% { opacity: 0; } 35% { opacity: .5; } 50% { opacity: .3; } 100% { opacity: 1; } }
@keyframes ember-breathe { 0%, 100% { opacity: .55; } 30% { opacity: .95; } 55% { opacity: .7; } 78% { opacity: 1; } }

/* the settled ash along the foot of the page, deeper as the leaf burns; the folio stays clear */
.pg-ash { position: absolute; left: 0; right: 0; bottom: 0; height: 8%; z-index: 0; pointer-events: none; opacity: .45; background: url(/public/world/book/ash-settle.webp?v=1cb18d60ac) 0 100% / 100% 100% no-repeat; }
.burn-2 .pg-ash { opacity: .75; } .burn-3 .pg-ash { opacity: 1; }
/* a turn lands: a puff of four flakes off the bottom band (public/book-burn.js sets .is-puff for half a second) */
.pg-ash::before, .pg-ash::after, .pg-burn::before, .pg-burn::after { content: ""; position: absolute; width: 6px; height: 6px; opacity: 0; background: url(/public/world/book/ash-flakes.webp?v=96f36a2ba8) 0 0 / 48px 6px no-repeat; }
.pg-ash::before { left: 18%; bottom: 40%; --px: -10px; }
.pg-ash::after { left: 74%; bottom: 30%; --px: 9px; background-position: -18px 0; }
.pg-burn::before { left: 31%; bottom: 3%; --px: -6px; background-position: -36px 0; }
.pg-burn::after { left: 66%; bottom: 2.5%; --px: 7px; background-position: -24px 0; }
.is-puff .pg-ash::before, .is-puff .pg-ash::after, .is-puff .pg-burn::before, .is-puff .pg-burn::after { animation: ash-puff .5s ease-out both; }
.is-puff .pg-ash::after, .is-puff .pg-burn::after { animation-delay: .06s; }
@keyframes ash-puff { 0% { opacity: 0; transform: none; } 25% { opacity: .9; } 100% { opacity: 0; transform: translate(var(--px), -16px) rotate(80deg); } }

/* the ash that falls ON the page (public/book-burn.js): flakes drift down onto the paper as if from above the reader,
   land where they fall, lie a while (a faint ember at the edge, in the element's hue, where they lie clear of the text)
   and crumble. They live in .pg-fall inside the page face, above the text, so they turn with the fold. The script keeps
   at most two over the text at once, small and half-transparent, and one landed there crumbles within 2 s. */
.pg-fall { position: absolute; inset: 0; z-index: 2; overflow: hidden; pointer-events: none; }
.af { position: absolute; left: 0; top: 0; width: var(--s); height: var(--s); opacity: 0; }
.af-img { position: absolute; inset: 0; background: url(/public/world/book/ash-flakes.webp?v=96f36a2ba8) calc(var(--k) * var(--s) * -1) 0 / auto var(--s) no-repeat; }
.af-glow { position: absolute; inset: -45%; border-radius: 50%; opacity: 0; background: radial-gradient(closest-side, var(--ember) 0, var(--ember) 30%, transparent 100%); }

/* margin and desk flakes over the stage (also the no-script ash): a layer that reaches 40 px above the page and over
   the desk strip on its right; each is a sprite flake at the top of a full-height column, so translateY(%) is a share
   of the layer, and --land sets it down in the bottom band where it fades into the settled ash */
.book-ash { position: absolute; top: -40px; right: -16px; bottom: 0; left: 0; z-index: 4; overflow: hidden; pointer-events: none; contain: strict; transition: opacity .6s ease; }
.book-ash > i { display: none; position: absolute; top: 0; left: var(--x); width: var(--s, 3px); height: 100%; opacity: 0; }
.book-ash > i:nth-child(-n+4) { background: url(/public/world/book/ash-flakes.webp?v=96f36a2ba8) calc(var(--k) * var(--s) * -1) 0 / auto var(--s) no-repeat; transform-origin: 50% calc(var(--s) / 2); animation: ash-fall var(--dur) linear var(--del) infinite; }
.book-ash > i:nth-child(1) { --x: 6px; --s: 9px; --k: 0; --dur: 12s; --del: -2s; --sx: 4px; --land: 96%; --r: 280deg; }
.book-ash > i:nth-child(2) { --x: calc(100% - 12px); --s: 11px; --k: 2; --dur: 10s; --del: -6s; --sx: 3px; --land: 98%; --r: 320deg; }
.book-ash > i:nth-child(3) { --x: calc(100% - 6px); --s: 8px; --k: 5; --dur: 15s; --del: -9s; --sx: -3px; --land: 97%; --r: -300deg; }
.book-ash > i:nth-child(4) { --x: calc(100% - 30px); --s: 10px; --k: 1; --dur: 13s; --del: -4s; --sx: -4px; --land: 97.5%; --r: -240deg; }
@keyframes ash-fall {
  0% { opacity: 0; transform: translate(0, 0) rotate(0); }
  6% { opacity: .9; }
  25% { transform: translate(var(--sx), 22%) rotate(calc(var(--r) * .25)); }
  50% { transform: translate(calc(var(--sx) * -1), 46%) rotate(calc(var(--r) * .5)); }
  75% { transform: translate(var(--sx), 70%) rotate(calc(var(--r) * .8)); }
  88% { opacity: .9; transform: translate(0, var(--land)) rotate(var(--r)); }
  100% { opacity: 0; transform: translate(0, var(--land)) rotate(var(--r)); }
}
/* sparks (5-6): from the threshold on, a mote of the ember colour lifts out of the bottom band and fades */
.book-ash > i:nth-child(5), .book-ash > i:nth-child(6) { top: auto; bottom: 1%; height: 5.5%; animation: ash-spark var(--dur) ease-out var(--del) infinite; }
.book-ash > i:nth-child(5)::before, .book-ash > i:nth-child(6)::before { content: ""; position: absolute; left: 0; bottom: 0; width: 2px; height: 2px; border-radius: 50%; background: var(--ember); box-shadow: 0 0 3px 1px var(--ember); }
.book-ash > i:nth-child(5) { --x: 13%; --dur: 4.6s; --del: -1s; --sx: 5px; }
.book-ash > i:nth-child(6) { --x: 81%; --dur: 6.2s; --del: -4s; --sx: -4px; }
@keyframes ash-spark { 0%, 45% { opacity: 0; transform: translate(0, 0); } 55% { opacity: 1; } 100% { opacity: 0; transform: translate(var(--sx), -100%); } }

/* how much falls in the margins: one on the cover and a locked page, three from the front on, sparks from the threshold */
.book-read[data-burn="0"] .book-ash > i:nth-child(1),
.book-read[data-burn="1"] .book-ash > i:nth-child(-n+3),
.book-read[data-burn="2"] .book-ash > i:nth-child(-n+3), .book-read[data-burn="2"] .book-ash > i:nth-child(5), .book-read[data-burn="2"] .book-ash > i:nth-child(6),
.book-read[data-burn="3"] .book-ash > i:nth-child(-n+3), .book-read[data-burn="3"] .book-ash > i:nth-child(5), .book-read[data-burn="3"] .book-ash > i:nth-child(6) { display: block; }
@media (min-width: 721px) {
  .book-read[data-burn="2"] .book-ash > i:nth-child(4), .book-read[data-burn="3"] .book-ash > i:nth-child(4) { display: block; }
}
/* the last page: the last flakes fall once and settle, then the page is still */
.book-read[data-burn="out"] .book-ash > i:nth-child(-n+4) { display: block; animation-name: ash-fall-last; animation-iteration-count: 1; animation-delay: calc(var(--k) * .4s); animation-fill-mode: both; }
@keyframes ash-fall-last {
  0% { opacity: 0; transform: translate(0, 0) rotate(0); }
  6% { opacity: .9; }
  25% { transform: translate(var(--sx), 22%) rotate(calc(var(--r) * .25)); }
  50% { transform: translate(calc(var(--sx) * -1), 46%) rotate(calc(var(--r) * .5)); }
  75% { transform: translate(var(--sx), 70%) rotate(calc(var(--r) * .8)); }
  88% { opacity: .9; transform: translate(0, var(--land)) rotate(var(--r)); }
  100% { opacity: 0; transform: translate(0, var(--land)) rotate(var(--r)); }
}

/* rest and pause (public/book-burn.js): a phone lets the ash rest after 45 s on a page; off screen or a hidden tab pauses */
.book-read.ash-rest .book-ash { opacity: 0; transition-duration: 2.4s; }
.book-read.ash-pause .book-ash > i, .book-read.ash-pause .pg-ember { animation-play-state: paused; }

/* still: reduced motion, and the lite mode for Save-Data or a low-memory device */
@media (prefers-reduced-motion: reduce) {
  .book-ash, .pg-fall { display: none; }
  .book-read .pg-burn { animation: none !important; }
  .book-read .pg-ember { animation: none !important; opacity: .6; }
  .pg-ash::before, .pg-ash::after, .pg-burn::before, .pg-burn::after { display: none; }
}
/* 설정 > 화면 효과 off: the same stillness as reduced motion (book-burn.js reads ddPrefs.quiet()) */
html[data-fx="off"] .book-ash, html[data-fx="off"] .pg-fall { display: none; }
html[data-fx="off"] .book-read .pg-burn { animation: none !important; }
html[data-fx="off"] .book-read .pg-ember { animation: none !important; opacity: .6; }
html[data-fx="off"] .pg-ash::before, html[data-fx="off"] .pg-ash::after, html[data-fx="off"] .pg-burn::before, html[data-fx="off"] .pg-burn::after { display: none; }
@media (prefers-reduced-data: reduce) {
  .book-ash, .pg-fall { display: none; }
  .book-read .pg-burn { animation: none !important; }
  .book-read .pg-ember { animation: none !important; opacity: .6; }
}
.book-read.ash-lite .book-ash, .book-read.ash-lite .pg-fall { display: none; }
.book-read.ash-lite .pg-burn { animation: none !important; }
.book-read.ash-lite .pg-ember { animation: none !important; opacity: .6; }
.book-read.ash-lite .pg-ash::before, .book-read.ash-lite .pg-ash::after, .book-read.ash-lite .pg-burn::before, .book-read.ash-lite .pg-burn::after { display: none; }

/* ---- holes (plan v3): the embers burn through ---- */
.is-boss { --bh-halo-a: .04; } /* the faint outer scorch where it may graze text: checked against 7:1 by the build */
.pg-holes { position: absolute; inset: 0; z-index: 1; pointer-events: none; }
.burn-1 .pg-holes { --slot-s: 13px; } .burn-2 .pg-holes { --slot-s: 16px; } .burn-3 .pg-holes { --slot-s: 19px; }
.bh { position: absolute; left: calc(var(--x) - var(--s) / 2); top: calc(var(--y) - var(--s) / 2); width: var(--s); height: var(--s); border-radius: 50%;
  box-shadow: 0 0 calc(var(--s) * .45) calc(var(--s) * .15) rgba(96, 50, 18, var(--bh-halo-a)); } /* the faint outer halo */
/* cold: the scorch (::before, the hole's own shape grown by the 3 px the placer keeps clear) and the hole (::after);
   forming: the same drawn by children that animate */
.bh::before, .bh-scorch { content: ""; position: absolute; inset: -3px; background: rgba(70, 34, 8, .62); transform: rotate(var(--r, 0deg));
  -webkit-mask: url(/public/world/book/bh-holes.webp?v=cb44760f8d) calc(var(--k) * (var(--s) + 6px) * -1) 0 / calc((var(--s) + 6px) * 6) calc(var(--s) + 6px) no-repeat; mask: url(/public/world/book/bh-holes.webp?v=cb44760f8d) calc(var(--k) * (var(--s) + 6px) * -1) 0 / calc((var(--s) + 6px) * 6) calc(var(--s) + 6px) no-repeat; }
.bh::after, .bh-hole { content: ""; position: absolute; inset: 0; transform: rotate(var(--r, 0deg)); background: url(/public/world/book/bh-holes.webp?v=cb44760f8d) calc(var(--k) * var(--s) * -1) 0 / calc(var(--s) * 6) var(--s) no-repeat; }
.bh-rim { position: absolute; inset: 0; opacity: 0; background: var(--ember); transform: rotate(var(--r, 0deg));
  -webkit-mask: url(/public/world/book/bh-rim.webp?v=5013e671cb) calc(var(--k) * var(--s) * -1) 0 / calc(var(--s) * 6) var(--s) no-repeat; mask: url(/public/world/book/bh-rim.webp?v=5013e671cb) calc(var(--k) * var(--s) * -1) 0 / calc(var(--s) * 6) var(--s) no-repeat; }
.bh-core { position: absolute; left: 50%; top: 50%; width: 3px; height: 3px; margin: -1.5px 0 0 -1.5px; border-radius: 50%; opacity: 0; background: #fff8e6; box-shadow: 0 0 3px 2px var(--ember); }
.bh.is-forming::before, .bh.is-forming::after { display: none; }
.bh.is-forming .bh-scorch, .bh.is-forming .bh-hole { opacity: 0; }
/* the old holes: 8 slots on the outer rim, the top and the foot, a third over the edge (the face clips it into a bite) */
.bh[class*="bh-s"] { --s: var(--slot-s, 12px); }
.bh-s1 { --x: calc(100% - var(--s) * .3); --y: 16%; } .bh-s2 { --x: calc(100% - var(--s) * .3); --y: 47%; } .bh-s3 { --x: calc(100% - var(--s) * .3); --y: 74%; }
.bh-s4 { --x: 18%; --y: calc(100% - var(--s) * .3); } .bh-s5 { --x: 76%; --y: calc(100% - var(--s) * .3); } .bh-s6 { --x: 62%; --y: calc(var(--s) * .3); }
.bh.bh-s7 { --x: calc(100% - 11px); --y: calc(100% - 11px); --s: calc(var(--slot-s, 12px) * .8); } .bh-s8 { --x: 30%; --y: calc(var(--s) * .3); }
.bh-k0 { --k: 0; --r: 0deg; } .bh-k1 { --k: 1; --r: 70deg; } .bh-k2 { --k: 2; --r: 160deg; } .bh-k3 { --k: 3; --r: 230deg; } .bh-k4 { --k: 4; --r: 300deg; } .bh-k5 { --k: 5; --r: 20deg; }
/* the back of a turning leaf shows the same holes, seen from behind */
.pg-holes.is-back { z-index: 0; }
.pg-holes.is-back .bh::after { transform: rotate(var(--r, 0deg)) scaleX(-1); }

/* an ember falling onto the paper: a small flake lit all the way down, a white-hot core */
.af.is-ember .af-glow { opacity: .9; inset: -70%; }
.af.is-ember .af-img { inset: 15%; opacity: .55; }
.af.is-ember::after { content: ""; position: absolute; left: 50%; top: 50%; width: 3px; height: 3px; margin: -1.5px 0 0 -1.5px; border-radius: 50%; background: #fff6dc; box-shadow: 0 0 2px 1px var(--ember); }
.af-speck { position: absolute; left: 0; top: 0; width: 5px; height: 5px; border-radius: 50%; opacity: 0; background: radial-gradient(closest-side, rgba(60, 30, 10, .5), rgba(60, 30, 10, 0)); }
