/* ═══ HELHEIMRS — fx layer (portal, mist, lantern secrets, ravens, dice, guide, eggs, tools) ═══ */
/* z-index 1 puts the page above the footer that follows it: a menu or picker
   opened near the end of a short page (an admin row's ⋯ when one member is
   found) runs past #app's bottom, and the footer — positioned, later in the
   document — used to paint over it and take its clicks. Every fixed layer
   (header, bottom bar, tray, modals, ravens) sits at --z-nav or higher. */
#app { position: relative; isolation: isolate; z-index: 1; }
/* ── realm skies ──
   A night sky behind each realm's front page: dark and cold on the reading
   side (inline-start, under the title), a realm's own light on the far side,
   dissolving downward into the ground so the page never ends at a hard edge.
   They used to be public-domain landscape paintings — a Norwegian fjord over
   the hall, a farmed valley over the quests — the living Romantic North, not
   Helheim, and the same plates the card covers used. Now each is a plate drawn
   in the house style (assets/art/sky-*.svg: stone ranges, frost fog, one
   emerald light): the hall's lit ridge, the road to a far gate, the Norns'
   three threads, Niflheim's still water. It sits under everything (z-index -1
   inside #app's isolation) and never moves. */
/* A gradient has no logical keyword, so the dark side is a variable the document direction sets. */
#app[data-sky] { --sky-dark-to: left; --sky-far: 22%; }
[dir="ltr"] #app[data-sky] { --sky-dark-to: right; --sky-far: 78%; }
#app[data-sky]::before {
  content: ""; position: absolute; inset-inline: 0; top: 0; z-index: -1; pointer-events: none;
  height: clamp(420px, 78vh, 760px);
  background-image:
    linear-gradient(to var(--sky-dark-to), rgba(10,10,11,.92) 0%, rgba(10,10,11,.62) 40%, rgba(10,10,11,.18) 100%),
    var(--sky),
    linear-gradient(180deg, #0B0E0E 0%, #0D1211 55%, #0A0A0B 100%);
  /* the plate covers the box and leans to the far side, where its light is */
  background-size: 100% 100%, cover, 100% 100%;
  background-position: 0 0, var(--sky-far) 38%, 0 0;
  background-repeat: no-repeat;
  -webkit-mask-image: linear-gradient(to bottom, #000 0%, #000 45%, transparent 100%);
  mask-image: linear-gradient(to bottom, #000 0%, #000 45%, transparent 100%);
}
#app[data-sky="hall"] { --sky: url(../assets/art/sky-hall.svg); }
#app[data-sky="quests"] { --sky: url(../assets/art/sky-quests.svg); }
#app[data-sky="fate"] { --sky: url(../assets/art/sky-fate.svg); }
#app[data-sky="niflheim"] { --sky: url(../assets/art/sky-niflheim.svg); }
/* the phone reads top-down, not side-by-side: grade from the top instead */
@media (max-width: 760px) {
  #app[data-sky]::before {
    height: clamp(360px, 62vh, 560px);
    background-image: linear-gradient(to bottom, rgba(10,10,11,.4) 0%, rgba(10,10,11,.7) 60%, rgba(10,10,11,.92) 100%), var(--sky), linear-gradient(180deg, #0B0E0E, #0A0A0B);
    background-position: 0 0, 50% 30%, 0 0;
  }
}
/* Frost: the same plate washed to pale stone, so dark type still reads */
:root[data-theme="light"] #app[data-sky]::before {
  background-image: linear-gradient(to var(--sky-dark-to), rgba(230,232,226,.97) 0%, rgba(230,232,226,.88) 45%, rgba(230,232,226,.74) 100%), var(--sky), linear-gradient(180deg, #DCE2DE 0%, #E2E6E1 60%, #E6E8E2 100%);
}
@media (max-width: 760px) {
  :root[data-theme="light"] #app[data-sky]::before { background-image: linear-gradient(to bottom, rgba(230,232,226,.8), rgba(230,232,226,.97)), var(--sky), linear-gradient(180deg, #DCE2DE, #E6E8E2); }
}

/* ── empty states: the Gatekeeper rises once, his eyes light, the way on follows — then he is still ── */
:root[data-motion="on"] .empty > .keeper { animation: hx-keeper-rise 480ms var(--ease-out) both; }
:root[data-motion="on"] .empty > .keeper .keeper-eye { animation: hx-keeper-eyes 400ms 780ms var(--ease-out) both; }
:root[data-motion="on"] .empty > :is(.btn, a, .row) { animation: hx-keeper-rise 360ms 930ms var(--ease-out) both; }
@keyframes hx-keeper-rise { from { opacity: 0; transform: translateY(12px); } to { opacity: 1; transform: none; } }
@keyframes hx-keeper-eyes { from { opacity: 0; } to { opacity: 1; } }

/* ── Portal transition (fx/portal.js) ──
   One SVG: the veil in the reader's own ground colour with the gate cut out of
   it, and the gate's rim. The emerald is the 2px rim and the light it throws
   inward (two strokes clipped to the inside of the gate: 18px and 6px deep) —
   never a fill. Frost gets a jade rim on its pale ground, never a dark frame. */
.gate-x { position: absolute; inset: 0; width: 100%; height: 100%; overflow: visible; pointer-events: none; }
.gate-x__veil { fill: var(--bg); }
.gate-x__rim { fill: none; stroke: var(--emerald); stroke-width: 2; stroke-linejoin: miter; }
.gate-x__g1 { fill: none; stroke: rgba(47,211,154,.12); stroke-width: 36; stroke-linejoin: round; }
.gate-x__g2 { fill: none; stroke: rgba(47,211,154,.24); stroke-width: 12; stroke-linejoin: round; }
:root[data-theme="light"] .gate-x__rim { stroke: var(--jade); }
:root[data-theme="light"] .gate-x__g1 { stroke: rgba(47,211,154,.14); }
:root[data-theme="light"] .gate-x__g2 { stroke: rgba(15,107,79,.2); }
/* dagaz, drawn (portal.js, a 40px stave) and centred on the gate's middle; a
   stroke takes no text-shadow, so its light is a drop-shadow */
.gate-x__rune { position: absolute; left: 0; top: 0; display: grid; place-items: center; width: 48px; height: 48px; margin: -24px 0 0 -24px; font-family: var(--font-rune); font-size: 48px; line-height: 1; color: var(--emerald); opacity: 0; pointer-events: none; }
.gate-x__rune svg { display: block; filter: drop-shadow(0 0 9px rgba(47,211,154,.7)); }
:root[data-theme="light"] .gate-x__rune { color: var(--jade); }
:root[data-theme="light"] .gate-x__rune svg { filter: drop-shadow(0 0 7px rgba(47,211,154,.45)); }

/* ── the chisel (HX.fx.carve) ── fresh-cut stone glows emerald, then settles */
.hx-carving { color: var(--accent-text) !important; text-shadow: 0 0 18px rgba(47,211,154,.5) !important; }
.hx-sparks { position: fixed; left: 0; top: 0; z-index: var(--z-raven); pointer-events: none; }
.hx-sparks i { position: absolute; width: 3px; height: 3px; margin: -1.5px; border-radius: 50%; background: var(--emerald-soft); box-shadow: 0 0 6px var(--emerald); }

/* ── Mist of Niflheim (fx/fog.js) ── a fog texture in a radial mask: body, and no edge anywhere */
[data-mist] { position: relative; }
.mist-veil { position: absolute; inset: -8% -3%; z-index: 6; pointer-events: none; overflow: hidden; }
.mist-veil i { position: absolute; top: -4%; bottom: -4%; width: 64%; opacity: .8; background-size: cover; background-repeat: no-repeat; mix-blend-mode: screen; will-change: transform, opacity;
  -webkit-mask-image: radial-gradient(closest-side, #000 35%, rgba(0,0,0,.6) 62%, transparent 100%); mask-image: radial-gradient(closest-side, #000 35%, rgba(0,0,0,.6) 62%, transparent 100%);
  transition: transform 1.1s cubic-bezier(.2,.7,.2,1), opacity 1.1s cubic-bezier(.2,.7,.2,1); }
.mist-veil i.l { left: -8%; }                 /* physical sides: the halves part outward, in either direction */
.mist-veil i.r { right: -8%; transition-delay: 90ms; }
:root[data-theme="light"] .mist-veil i { mix-blend-mode: multiply; opacity: .75; }
/* Parted, the halves thin to nothing as they slide, in both themes. The Frost
   rule above is as specific as these and used to come after them, so in Frost
   the halves slid out still at 75% and the grey vanished in one frame when the
   veil was removed. Now the veil goes only once the right half has finished
   fading (fog.js), so removing it changes no pixel. */
:root .mist-veil.is-parted i.l { transform: translate(-42%, -20px); opacity: 0; }
:root .mist-veil.is-parted i.r { transform: translate(42%, -20px); opacity: 0; }

/* ── Ravens: the one message stack (fx/ravens.js) ──
   Low on the far side of the reading column (inline-end), clear above the
   sound pill when the music plays: it used to sit at the top of the reading side, over the page's
   own heading and breadcrumbs. The d20's corner belongs to the tray and the
   Gatekeeper. On a phone it sits above the bottom nav; with the tray open
   there (full width) it goes to the top. */
.raven-msg { top: auto; bottom: calc(var(--s-6) + 44px + var(--s-3)); inset-inline-start: auto; inset-inline-end: var(--s-6); width: min(360px, calc(100vw - 2 * var(--s-4))); display: flex; flex-direction: column; gap: var(--s-3); z-index: calc(var(--z-raven) - 1); pointer-events: none; }
.raven-msg > * { pointer-events: auto; }
/* A new message goes into an open dialog (ravens.js); one already here when a
   dialog opens, and a bird still on its way to it, sink under the veil with the
   page instead of covering the dialog, and come back up when it closes (a
   closing dialog is inert at once, so they do not wait for its exit). */
body:has(> .modal-backdrop:not([inert])) :is(.raven-msg, .raven-layer) { z-index: calc(var(--z-modal) - 1); }
@media (max-width: 900px) {
  .raven-msg { bottom: calc(var(--bnav-h, 64px) + env(safe-area-inset-bottom) + var(--s-3)); inset-inline: var(--s-3); width: auto; }
  .raven-msg.is-aside { top: calc(64px + var(--s-3)); bottom: auto; }
}
.raven-note { position: relative; }
.raven-note.has-bird { margin-top: 46px; }
/* written while the gate is closing (ravens.js): held, unseen, until it has opened */
.raven-note.is-waiting { opacity: 0; pointer-events: none; }
/* The card itself is components.css's toast (a grid: rune · words · close);
   ravens.js plays its entrance, so the CSS one is off. The action sits under
   the words, in their column. */
.raven-note .toast { animation: none; }
.toast__act { grid-column: 2; justify-self: start; margin-top: 6px; color: var(--accent-text); font-weight: 700; font-size: var(--fs-sm); }
button.toast__act { padding: 0; border: 0; background: none; font-family: inherit; line-height: inherit; cursor: pointer; }
button.toast__act:hover { text-decoration: underline; text-underline-offset: 3px; }
@media (pointer: coarse) { .toast__act { min-height: 44px; display: inline-flex; align-items: center; margin-top: 0; } }
@keyframes hx-drop { from { opacity: 0; transform: translateY(-28px) scale(.92); } to { opacity: 1; transform: none; } }
/* The still raven, perched on the card's corner when motion is off — the pose
   the flying one stands up into and holds (ravens.js perchOn): toes closed on
   the top edge (they close at 105 of the art's 120), feet a quarter of the bird
   in from the corner, facing the words, its tail hanging past the edge. */
.raven-note__bird { position: absolute; top: -55px; inset-inline-start: -16px; width: 64px; pointer-events: none; z-index: 1; }
/* a phone's card runs to 12px from the screen's edge: the tail stays on screen */
@media (max-width: 900px) { .raven-note__bird { inset-inline-start: -8px; } }
.raven-note__bird svg { display: block; }
.raven-note__bird svg.is-flipped { transform: scaleX(-1); }
/* A black bird on a near-black page read as nothing at all: a frost rim and a
   soft halo (and the gate's emerald further out) make it a raven at a glance.
   On Frost the dark bird already stands out; it gets a lift, not a glow. */
.raven svg, .raven-note__bird svg { filter: drop-shadow(0 0 .8px rgba(237,239,234,.9)) drop-shadow(0 0 5px rgba(237,239,234,.3)) drop-shadow(0 0 14px rgba(47,211,154,.4)); }
:root[data-theme="light"] .raven svg, :root[data-theme="light"] .raven-note__bird svg { filter: drop-shadow(0 0 .8px rgba(255,255,255,.95)) drop-shadow(0 3px 6px rgba(14,16,15,.22)) drop-shadow(0 0 10px rgba(47,211,154,.28)); }
.raven__letter { position: absolute; transform: rotate(-10deg); transform-origin: 50% 0; filter: drop-shadow(0 0 4px rgba(237,239,234,.55)); pointer-events: none; }
[dir="ltr"] .raven__letter { transform: rotate(10deg); }
:root[data-theme="light"] .raven__letter { filter: drop-shadow(0 1px 2px rgba(14,16,15,.35)); }

/* ── Dice tray extras ── */
/* fx/dice.js grows the tray out of the d20 and puts it back; no CSS entrance */
.tray, .tray-backdrop { animation: none; }
.tray__result { opacity: 0; transition: opacity var(--t-ui) var(--ease-out); }
.tray__result.is-on { opacity: 1; }
.tray__result .total.is-pop { animation: hx-pop .5s var(--ease-out) both; }
@keyframes hx-pop { 0% { transform: scale(.6); opacity: 0; } 60% { transform: scale(1.12); opacity: 1; } 100% { transform: none; } }
.tray__slab .hint { position: absolute; inset: 0; display: grid; place-items: center; color: #9EA39C; font-size: var(--fs-sm); pointer-events: none; transition: opacity var(--t-ui) var(--ease-out); }
.tray__slab .hint.is-off { opacity: 0; }
/* the log has its room from the first open, so a result never moves the tray */
.tray__log { min-height: 96px; align-content: start; }
.tray__log div { display: flex; gap: 6px; align-items: baseline; }
.tray__log div.is-new { animation: hx-logrow 200ms var(--ease-out) both; }
@keyframes hx-logrow { from { opacity: 0; transform: translateY(-4px); } to { opacity: 1; transform: none; } }
.tray__log-empty { color: var(--fg-faint); }
.tray__crit { display: inline-flex; align-self: center; }
.tray__crit svg { width: 16px; height: 16px; }
.tray__crit--nat20 { color: var(--emerald); filter: drop-shadow(0 0 4px rgba(47,211,154,.45)); }
:root[data-theme="light"] .tray__crit--nat20 { color: var(--jade); filter: none; }
.tray__crit--nat1 { color: var(--danger); }
.tray__log .expr { font-family: ui-monospace, 'SF Mono', Menlo, Consolas, monospace; direction: ltr; unicode-bidi: isolate; }
.tray__err { margin: calc(-1 * var(--s-2)) 0 0; color: var(--danger); font-size: var(--fs-xs); font-weight: 600; }
.tray__adv { display: flex; gap: var(--s-2); }
/* on a session's page: send the roll to its shared table instead (fx/dice.js) */
.tray__attable { font-size: var(--fs-sm); }
.tray__attable-note { display: block; color: var(--fg-muted); font-size: var(--fs-xs); font-weight: 400; }
.tray__adv .chip { min-height: 36px; }
.tray__chip b .tag { font-size: var(--fs-2xs); font-weight: 700; color: var(--accent-text); margin-inline-start: 2px; }
@media (pointer: coarse) { .tray__adv .chip { min-height: 44px; } }
.is-shaking { animation: hx-shake .5s linear both; }
@keyframes hx-shake { 10%, 90% { transform: translate(-2px, 1px); } 20%, 80% { transform: translate(3px, -2px); } 30%, 70% { transform: translate(-4px, 2px); } 40%, 60% { transform: translate(4px, -1px); } 50% { transform: translate(-3px, 3px); } }
/* crit: the face flares, the rim lights, embers rise off the slab, runes ring the die */
.tray__slab { transition: box-shadow .5s var(--ease-out); }
.tray__slab.is-crit { box-shadow: inset 0 0 0 1px rgba(47,211,154,.75), inset 0 0 36px rgba(47,211,154,.28); transition-duration: .12s; }
.tray__flare { position: absolute; left: 0; top: 0; width: 140px; height: 140px; margin: -70px 0 0 -70px; border-radius: 50%; background: radial-gradient(closest-side, rgba(123,230,192,.55), rgba(47,211,154,.18) 55%, transparent); pointer-events: none; mix-blend-mode: screen; }
.tray__embers { position: absolute; inset: 0; pointer-events: none; overflow: hidden; }
.tray__embers i { position: absolute; bottom: 4px; width: 4px; height: 4px; border-radius: 50%; background: var(--emerald-soft); box-shadow: 0 0 8px var(--emerald), 0 0 2px #fff; }
.tray__ring { position: absolute; left: 0; top: 0; width: 0; height: 0; pointer-events: none; }
/* drawn runes (dice.js), a 16px stave; the glow is a drop-shadow, since a stroke takes no text-shadow */
.tray__ring span { position: absolute; left: 0; top: 0; display: grid; place-items: center; width: 12px; height: 16px; margin: -8px 0 0 -6px; color: var(--emerald-soft); filter: drop-shadow(0 0 4px var(--emerald)); transform: rotate(var(--a)) translateY(calc(-1 * var(--r, 60px))); }
.tray__ring svg { display: block; }
/* The slab's carved alphabet (dice.js .tray__alpha): the full futhark as drawn
   staves, cut faint into the stone. It replaces layout.css's ::before, which
   set it in the rune font (ᚲ ᛃ ᛊ ᛜ as specks); the canvas is lifted over it so
   the dice roll across the carving rather than under it. */
.tray__slab.has-alpha::before { content: none; }
.tray__slab.has-alpha canvas { position: relative; }
.tray__alpha { position: absolute; inset: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 27px; color: rgba(255,255,255,.045); direction: ltr; pointer-events: none; user-select: none; }
.tray__alpha span { display: flex; gap: 20px; }
.tray__alpha svg { display: block; }
/* the numerals in the result line and the legend are drawn too (dice.js numSVG):
   the gate mark's height in the line, its glow as a drop-shadow */
.tray__result .runes > span { display: block; line-height: 0; }
.tray__result .runes svg.rune-glyph { display: block; filter: drop-shadow(0 0 5px rgba(47,211,154,.45)); }
.tray__runes b { display: grid; place-items: center; min-height: 22px; }
.tray__runes svg { display: block; }
.crit-banner { position: fixed; z-index: calc(var(--z-tray) + 2); pointer-events: none; white-space: nowrap; font-family: var(--font-display); font-weight: 900; font-size: clamp(1.8rem, 5vw, 2.6rem); line-height: 1.2; color: var(--emerald); text-shadow: 0 0 18px rgba(47,211,154,.55), 0 2px 10px rgba(0,0,0,.6); opacity: 0; }
:root[data-theme="light"] .crit-banner { color: var(--jade); text-shadow: 0 0 14px rgba(47,211,154,.4), 0 1px 0 #fff; }
.crit-vignette { position: fixed; inset: 0; z-index: var(--z-tray); pointer-events: none; opacity: 0; background: radial-gradient(ellipse at 50% 50%, transparent 45%, rgba(47,211,154,.5) 100%); }
/* fumble: the slab splits from the die, colour drains from the room, a hand comes for the die */
.tray__crackbox { position: absolute; inset: 0; pointer-events: none; }
.tray__cracks { position: absolute; inset: 0; width: 100%; height: 100%; overflow: visible; }
.tray__cracks path { fill: none; stroke: #C8CCC6; stroke-width: 1.6; stroke-linejoin: bevel; stroke-dasharray: 1; stroke-dashoffset: 1; filter: drop-shadow(0 1px 0 rgba(0,0,0,.8)); }
.tray__cracks path.b { stroke-width: 1; stroke: #9EA39C; }
.tray__slab.is-fumble .total { color: var(--ember-soft); }
.fumble-grey { position: fixed; inset: 0; z-index: var(--z-tray); pointer-events: none; opacity: 0; backdrop-filter: grayscale(1) contrast(.9); -webkit-backdrop-filter: grayscale(1) contrast(.9); }
.tray__hand { position: absolute; bottom: 0; left: 0; width: 104px; pointer-events: none; transform: translate(-50%, 100%); }
.tray__hand .ghost-hand { position: static; display: block; width: 100%; height: auto; transform: none; animation: none; opacity: .9; }

/* ── Gatekeeper guide ── */
.guide .speech p { margin: 0; line-height: 1.7; color: var(--fg-strong); }
.guide .speech .row { gap: 6px; }

/* ── Ragnarök ── the resting composition (what reduced motion sees); fx/eggs.js plays the way there */
.ragnarok { cursor: pointer; pointer-events: auto; overflow: hidden; }
.ragnarok > * { position: absolute; }
.rg-dim { inset: 0; background: #050506; opacity: .85; }
.rg-sky { inset: 0; background: radial-gradient(ellipse 75% 34% at 50% 80%, rgba(194,58,43,.34), transparent 72%), linear-gradient(180deg, #040405 0%, #07080A 42%, #120A09 70%, #1F0D09 80.5%, #050506 81%); }
.rg-glow { inset-inline: 0; bottom: 16%; height: 34%; background: radial-gradient(ellipse 58% 72% at 50% 100%, rgba(224,101,79,.7), rgba(194,58,43,.2) 50%, transparent 76%); opacity: .6; mix-blend-mode: screen; pointer-events: none; }
/* The crown climbs past the drawing's box (art.js lets it, about a fifth of its
   height); the padding gives it room inside the layer, so the burning copy's
   mask and glow reach the top of the crown instead of shearing it flat along the
   box's edge. The tree stands as tall as it can below the words (--rg-room is
   where they end, measured by eggs.js): on a wide or short screen it used to
   reach up behind the title and the sub-line, and a dark pool had to be laid
   behind them to keep them readable. Now it gives way instead; on a tall phone
   it was already clear and stays as it was. */
.rg-tree { --rg-h: min(72vh, calc(81.5vh - var(--rg-room, 30vh) - var(--s-3))); left: 50%; bottom: 18.5%; height: var(--rg-h); padding-top: calc(var(--rg-h) * .2); width: min(96vh, 118vw); translate: -50% 0; pointer-events: none; }
.rg-tree svg { display: block; width: 100%; height: 100%; }
/* the burning copy: the same silhouette with its veins alight, revealed from the roots up */
.rg-tree--lit { -webkit-mask: linear-gradient(to top, #000 0 50%, transparent 56%) 0 100% / 100% 200% no-repeat; mask: linear-gradient(to top, #000 0 50%, transparent 56%) 0 100% / 100% 200% no-repeat; filter: drop-shadow(0 0 5px rgba(47,211,154,.7)) drop-shadow(0 0 22px rgba(47,211,154,.3)); }
.rg-sea { inset-inline: 0; bottom: 0; height: 19%; background: linear-gradient(180deg, rgba(224,101,79,.16) 0, transparent 7%), linear-gradient(180deg, #0A0F0E 0%, #050607 40%, #030304 100%); box-shadow: 0 -1px 0 rgba(47,211,154,.3); }
.ragnarok .ragnarok__serpent { animation: none; transform-origin: 74% 100%; filter: drop-shadow(0 0 22px rgba(47,211,154,.4)); }
.rg-embers { inset: 0; pointer-events: none; }
/* at rest (motion off) the embers are a stipple on the ground; with motion they rise */
.rg-embers i { position: absolute; width: 3px; height: 3px; border-radius: 50%; background: var(--emerald-soft); box-shadow: 0 0 8px var(--emerald); opacity: .5; }
.rg-embers i.is-ember { background: #F0A08A; box-shadow: 0 0 8px var(--ember); }
.rg-words { inset-inline: 0; top: 5%; display: flex; flex-direction: column; align-items: center; gap: var(--s-3); padding-inline: var(--gutter); text-align: center; pointer-events: none; z-index: 1; }
/* The words stand in the open sky above the crown, with nothing laid behind
   them. The title gives up a little size on a short screen (12vh) so the tree
   keeps its height. The story is one line where it fits and even lines where
   it does not; the way out is its own quieter line under it. */
.ragnarok .ragnarok__title { position: static; animation: none; opacity: 1; line-height: 1.1; font-size: clamp(2.4rem, min(8vw, 12vh), 6rem); }
.ragnarok .ragnarok__sub { position: static; margin: 0; max-width: 30rem; color: var(--emerald-soft); font-size: var(--fs-md); line-height: 1.8; text-wrap: balance; text-shadow: 0 1px 10px #000, 0 0 2px #000; }
.ragnarok__hint { display: block; font-size: var(--fs-sm); opacity: .78; }
/* Wider than 16:9 the serpent's stage (1600×900, sliced to the width and
   bottom-anchored, art.js) lifts its head level with the words; its snout is
   226 units right of the centre, so the lines stay inside that, with room. */
@media (min-aspect-ratio: 16/9) {
  .ragnarok .ragnarok__sub { max-width: min(30rem, calc(100vw * 440 / 1600 - var(--s-6))); }
}

/* ── Mimic chest ── iron and basalt; the lock's emerald is its only colour */
.mimic { position: relative; display: inline-block; width: 52px; height: 46px; padding: 0; border: 0; background: none; cursor: pointer; vertical-align: middle; transform-origin: 50% 100%; }
.mimic svg { width: 52px; height: 46px; overflow: visible; }
.mimic__lock { filter: drop-shadow(0 0 3px rgba(47,211,154,.9)); }
.mimic__lid { transform-origin: 4px 20px; transition: transform .2s var(--ease-press); }
.mimic:hover .mimic__lid { transform: rotate(-8deg); }
.mimic__teeth, .mimic__eyes, .mimic__tongue { transition: opacity 90ms linear; }
/* open: the lid snaps up (90 ms) and the teeth and eyes come WITH it, not before */
.mimic.is-open .mimic__lid { transform: rotate(-56deg); transition-duration: 90ms; }
.mimic.is-open :is(.mimic__teeth, .mimic__eyes, .mimic__tongue) { opacity: 1 !important; }
.mimic.is-biting .mimic__lid { animation: hx-mimic-chomp .72s var(--ease-press) 90ms both; }
@keyframes hx-mimic-chomp { 0%, 100% { transform: rotate(-56deg); } 16%, 50%, 83% { transform: rotate(-3deg); } 33%, 66% { transform: rotate(-48deg); } }
.mimic__dmg { position: absolute; left: 50%; top: -4px; transform: translate(-50%, -18px); font-weight: 900; font-size: var(--fs-sm); color: var(--danger); text-shadow: 0 1px 4px rgba(0,0,0,.7); pointer-events: none; white-space: nowrap; }

/* ── Tools: name generator ── */
.tool { display: grid; gap: var(--s-4); }
.tool__bar { display: flex; flex-wrap: wrap; gap: var(--s-3); align-items: center; justify-content: space-between; }
.tool__group { display: flex; gap: var(--s-half); flex-wrap: wrap; align-items: center; }
.tool__group > span { font-size: var(--fs-xs); color: var(--fg-muted); margin-inline-end: 4px; }
.namegen__list { list-style: none; margin: 0; padding: 0; display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: var(--s-2); }
.namegen__list li { display: flex; align-items: center; justify-content: space-between; gap: var(--s-2); padding: var(--s-2) var(--s-3); background: var(--bg-elev); border: 1px solid var(--line); clip-path: var(--clip-facet); --facet: 8px; }
.namegen__list li b { font-family: var(--font-display); font-weight: 700; color: var(--fg-strong); }
.namegen__list li { animation: hx-drop .4s var(--ease-out) both; animation-delay: calc(var(--i, 0) * 50ms); }
.npc-card { display: grid; gap: var(--s-2); padding: var(--s-5); background: var(--stone-grad); border: 1px solid var(--line-glow); --facet: 14px; clip-path: var(--clip-facet); box-shadow: var(--shadow-stone), var(--glow-inset-soft); animation: rise .5s var(--ease-out) both; }
.npc-card :is(h3,h4) { margin: 0; font-size: var(--fs-xl); }
.npc-card .role { color: var(--accent-text); font-size: var(--fs-sm); }
.npc-card dl { margin: 0; display: grid; grid-template-columns: auto 1fr; gap: 4px var(--s-3); font-size: var(--fs-sm); }
.npc-card dt { color: var(--fg-muted); }
.npc-card dd { margin: 0; color: var(--fg); }

/* ── Tools: initiative tracker ── */
.init__add { display: grid; grid-template-columns: 2fr 1fr 1fr 1fr auto; gap: var(--s-2); align-items: end; }
.init__add .input { min-height: 40px; }
.init__add .input[type="number"] { direction: ltr; text-align: center; font-variant-numeric: tabular-nums; }
@media (max-width: 640px) { .init__add { grid-template-columns: 1fr 1fr; } .init__add .btn { grid-column: 1 / -1; } }
.init__bar { display: flex; flex-wrap: wrap; gap: var(--s-2); align-items: center; justify-content: space-between; padding: var(--s-2) 0; border-block: 1px solid var(--line); }
.init__bar .round { font-family: var(--font-display); font-weight: 800; color: var(--fg-strong); }
.init__bar .round b { color: var(--emerald); text-shadow: 0 0 10px rgba(47,211,154,.5); }
:root[data-theme="light"] .init__bar .round b { color: var(--jade); text-shadow: none; }
.init__list { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--s-2); }
/* One row, five columns: initiative | who (conditions under the name) | hit points | armour | a menu.
   It was seven controls abreast in ~600px; six combatants now fit in a 900px screen. */
.init__row { position: relative; display: grid; grid-template-columns: 44px minmax(140px, 1fr) 176px 48px 36px; grid-template-areas: "init who hp ac more"; gap: var(--s-3); align-items: center; min-height: 52px; padding: 6px var(--s-3); background: var(--bg-elev); border: 1px solid var(--line); border-inline-start: 3px solid transparent; clip-path: var(--clip-facet); --facet: 8px; transition: border-color var(--t-ui) var(--ease-out), box-shadow var(--t-ui) var(--ease-out), background var(--t-ui) var(--ease-out); }
.init__row.is-active { border-color: var(--line-glow); border-inline-start-color: var(--emerald); background: rgba(47,211,154,.07); box-shadow: var(--glow-inset); }
.init__row.is-down { opacity: .85; }
.init__row.is-down .init__init { opacity: .5; text-decoration: line-through; }
.init__row.is-monster .init__name::after { content: "هیولا"; font-size: var(--fs-2xs); font-weight: 600; color: var(--fg-muted); margin-inline-start: 6px; border: 1px solid var(--line); padding: 0 5px; }
[lang="en"] .init__row.is-monster .init__name::after { content: "monster"; }
.init__init { grid-area: init; display: grid; place-items: center; width: 44px; height: 40px; clip-path: var(--clip-hex); background: var(--bg-elev-3); font-family: var(--font-display); font-weight: 800; font-size: var(--fs-lg); color: var(--fg-strong); }
.init__row.is-active .init__init { background: var(--emerald-grad); color: var(--accent-fg); }
.init__who { grid-area: who; display: flex; flex-wrap: wrap; align-items: center; gap: 4px var(--s-2); min-width: 0; }
.init__name { font-weight: 700; color: var(--fg-strong); overflow-wrap: anywhere; }
.init__conds { display: flex; flex-wrap: wrap; gap: 4px; align-items: center; }
.init__conds .chip { min-height: 32px; padding: 0 8px; font-size: var(--fs-xs); }
.init__conds .select { min-height: 32px; width: auto; max-width: 100%; padding-inline: 8px 28px; font-size: var(--fs-xs); background-position: left 8px center; }
[dir="ltr"] .init__conds .select { background-position: right 8px center; }
.init__hp { grid-area: hp; display: grid; grid-template-columns: 36px minmax(0, 1fr) 36px; align-items: center; gap: 4px; font-variant-numeric: tabular-nums; }
.init__hp .btn { width: 36px; min-height: 36px; padding: 0; }
.init__num { display: grid; gap: 4px; justify-items: center; min-height: 36px; padding: 2px 4px; border: 1px solid transparent; background: none; color: var(--fg-strong); font: inherit; cursor: pointer; }
.init__num:hover { border-color: var(--line); }
.init__num b { white-space: nowrap; }
.init__num .muted { font-weight: 500; }
.init__hpbar { display: block; width: 100%; height: 4px; background: linear-gradient(to var(--bar-to, left), var(--emerald) calc(var(--p) * 100%), var(--line) 0); }
[dir="ltr"] .init__hpbar { --bar-to: right; }
.init__num.is-low b { color: var(--danger); }
.init__num.is-low .init__hpbar { background: linear-gradient(to var(--bar-to, left), var(--danger) calc(var(--p) * 100%), var(--line) 0); }
.init__delta { color: var(--accent-text); white-space: nowrap; animation: hx-logrow .2s var(--ease-out) both; }
.init__sethp, .init__hpedit { grid-column: 1 / -1; }
.init__sethp { justify-self: start; min-height: 36px; padding: 0 var(--s-2); border: 1px dashed var(--line-strong); background: none; color: var(--accent-text); font: inherit; font-size: var(--fs-xs); font-weight: 700; cursor: pointer; }
.init__hpedit { display: flex; gap: 4px; align-items: center; }
.init__hpedit .input { width: 64px; min-height: 36px; padding-inline: 6px; text-align: center; direction: ltr; font-variant-numeric: tabular-nums; }
.init__hpedit .btn { min-height: 36px; padding-inline: 8px; }
.init__ac { grid-area: ac; display: grid; place-items: center; }
.init__shield { display: grid; place-items: center; width: 40px; height: 44px; clip-path: polygon(50% 0, 100% 14%, 100% 58%, 50% 100%, 0 58%, 0 14%); background: var(--bg-elev-3); color: var(--fg-strong); font-weight: 800; font-variant-numeric: tabular-nums; }
.init__more { grid-area: more; }
.init__more > summary { display: grid; place-items: center; width: 36px; height: 36px; list-style: none; cursor: pointer; color: var(--fg-muted); font-size: 18px; border: 1px solid transparent; }
.init__more > summary::-webkit-details-marker { display: none; }
.init__more > summary:hover, .init__more[open] > summary { color: var(--fg-strong); border-color: var(--line); }
/* the menu lays itself over the row's own end (inside the row, so its facet never cuts it off) */
.init__menu { position: absolute; inset-block: 5px; inset-inline-end: calc(var(--s-3) + 40px); z-index: 2; display: flex; gap: 4px; align-items: center; padding-inline: 6px; background: var(--bg-elev-2); border: 1px solid var(--line-glow); box-shadow: var(--shadow-stone); }
.init__menu .btn { min-height: 34px; }
.init__menu .init__del { color: var(--danger); }
@media (max-width: 760px) {
  .init__row { grid-template-columns: 40px minmax(0, 1fr) 44px 36px; grid-template-areas: "init who who more" "init hp ac ac"; row-gap: 6px; }
  .init__hp { grid-template-columns: 36px minmax(0, 1fr) 36px; }
}
/* A DM runs the tracker on a tablet at the table, tapping hit points every round:
   28px was a miss more often than a hit. The row grows to fit rather than clipping. */
@media (pointer: coarse) {
  .init__hp .btn, .init__more > summary { width: 44px; min-height: 44px; height: 44px; }
  .init__num, .init__sethp, .init__hpedit .input, .init__hpedit .btn, .init__menu .btn { min-height: 44px; }
  .init__conds .chip { min-height: 44px; }
  .init__conds .select { min-height: 44px; }
}

/* ── Spirit lantern (fx/lantern.js) ── over a control the arrow is back and the dot steps aside */
/* the hidden runes' own layer: the page's content box, clipped, so they never lengthen the page */
.secret-layer { position: absolute; inset: 0; overflow: hidden; overflow: clip; pointer-events: none; }   /* clip: not a scroll box of its own */
/* each standing span holds words and a drawn rune (lantern.js); only a drawn
   rune shows its drawing. A 26px stave: as tall as the font's full runes were at
   28px, and every rune now that height. A stroke takes no text-shadow, so the
   glow is a drop-shadow. */
.secret-layer > span > svg { display: none; }
.secret-rune.is-drawn { line-height: 0; text-shadow: none; }
.secret-rune.is-drawn > svg { display: block; width: auto; height: 26px; filter: drop-shadow(0 0 6px var(--emerald)); }
.lantern-dot.is-hover { opacity: 0 !important; }
/* a glossary term, its tip or the palette under the pointer: the light steps away from the words */
body.is-over-term :is(.lantern, .lantern-dot) { opacity: 0 !important; }

/* ── the music player (fx/sound.js): over the floating pill on a desktop, or inside any menu ── */
/* Every control in it is the house's own: the options are .switch rows laid out
   like the menu's (words at the start, the switch at the end), and the level is
   drawn here rather than left to the browser. --sp-ground is the surface the
   panel sits on (the menu's inset well, or the pop-up's own stone), so the
   thumb's ring is cut from it. */
.sound-panel { --sp-ground: var(--bg-elev); display: grid; gap: var(--s-2); font-size: var(--fs-sm); }
.sound-panel__row { display: flex; gap: var(--s-2); }
.sound-panel__opt { display: flex; align-items: center; justify-content: space-between; gap: var(--s-3); min-height: 36px; }
/* The level: a 4px groove in --line-strong, filled in --accent-text up to the
   thumb (--v, set by sound.js) — 3:1 and more on either theme's panel, where the
   browser's emerald on Frost was 1.9:1. A range runs right to left on a Persian
   page, so its fill does too; an English page turns it. The thumb is the
   switch's own knob, a disc ringed in the panel's ground so it stands off the fill. */
.sound-panel__range { --fill-to: left; --thumb: 16px; -webkit-appearance: none; appearance: none; display: block; width: 100%; height: 24px; margin: 0; padding: 0; background: transparent; cursor: pointer; }
[dir="ltr"] .sound-panel__range { --fill-to: right; }
.sound-panel__range::-webkit-slider-runnable-track { height: 4px; background: linear-gradient(to var(--fill-to), var(--accent-text) var(--v, 45%), var(--line-strong) var(--v, 45%)); }
.sound-panel__range::-moz-range-track { height: 4px; background: var(--line-strong); }
.sound-panel__range::-moz-range-progress { height: 4px; background: var(--accent-text); }
.sound-panel__range::-webkit-slider-thumb { -webkit-appearance: none; appearance: none; box-sizing: border-box; width: var(--thumb); height: var(--thumb); margin-top: calc((4px - var(--thumb)) / 2); border: 2px solid var(--sp-ground); border-radius: 50%; background: var(--accent-text); box-shadow: 0 0 8px rgba(47,211,154,.55); transition: transform var(--t-fast) var(--ease-press); }
.sound-panel__range::-moz-range-thumb { box-sizing: border-box; width: var(--thumb); height: var(--thumb); border: 2px solid var(--sp-ground); border-radius: 50%; background: var(--accent-text); box-shadow: 0 0 8px rgba(47,211,154,.55); }
.sound-panel__range:hover::-webkit-slider-thumb, .sound-panel__range:active::-webkit-slider-thumb { transform: scale(1.15); }
/* (one rule per engine: a list holding the other engine's pseudo-element is dropped whole) */
:root[data-theme="light"] .sound-panel__range::-webkit-slider-thumb { box-shadow: none; }
:root[data-theme="light"] .sound-panel__range::-moz-range-thumb { box-shadow: none; }
/* a finger needs the switch's own 44px reach, and its bigger knob */
@media (pointer: coarse) { .sound-panel__range { --thumb: 24px; height: 44px; } .sound-panel__opt { min-height: 44px; } }
/* Inside a menu (the header's «نمایش», the phone's only way to the player) it
   lies in two rows where it took three: the level and «از اول» share one (the
   menu's own switch is play and stop, so the panel's play button is not there,
   shell.css), then «در پس‌زمینه هم پخش شود». Stacked, it was 210px tall at a
   finger's sizes and pushed every link of a 375px phone's menu below the fold. */
.sound-panel:not(.sound-panel--pop) { grid-template-columns: minmax(0, 1fr) auto; align-items: center; column-gap: var(--s-3); }
.sound-panel:not(.sound-panel--pop) > :is(.sound-panel__vol, .sound-panel__opt) { grid-column: 1 / -1; }

/* ── the header menu (shell.css .hdr-menu): its foot, once the player makes it scroll ──
   Taller than the screen, the phone's stopped 25px short of the bottom bar: page
   text showed in the gap under a half-cut row, and nothing said the menu
   scrolled. Now it runs down under the bar (by its corner's radius, so the
   rounded corners are tucked out of sight) and nothing of the page shows
   between them. A laptop's is cut just the same: 1366×768 with the player open
   shows 674 of its 890px, and 1280×700 slices through «پروفایل» with eight rows
   below, so the fade below is drawn at every width, not only on a phone. The
   menu hangs 6px under the header; --hm-max is its reach, set on the header so
   the fade can stand on the same line (a desktop's, 24px short of the window's
   foot, and the deeper foot padding the last row rests on, are shell.css's; a
   phone's is this one). --hm-foot is what of the menu's foot the fade stands
   above: the bottom border on a desktop, the corner tucked under the bar on a
   phone, where the fade's own corners stay square. */
.hdr { --hm-foot: 1px; --hm-foot-r: calc(var(--r-2) - 1px); }
@media (max-width: 56.25em) {
  .hdr { --hm-max: calc(100dvh - var(--hdr-h) - 6px - var(--bnav-h) - 1px - env(safe-area-inset-bottom) + var(--r-2)); --hm-foot: var(--r-2); --hm-foot-r: 0px; }
  .hdr-menu { max-height: var(--hm-max); }
}
/* While more of it is below, its cut edge fades into the menu's own stone
   (not into the page, which a mask would have shown through), just above the
   bar or the border. The fade follows the menu's scroll (a scroll timeline, no
   script): it thins out over the last 40px, and a menu that fits, whose
   timeline is inactive, keeps the resting opacity 0. It is the header's, laid
   over the menu's foot, rather than the menu's own because a sticky child of a
   grid cannot leave its row; placed from the header with the menu's own inset
   (shell.css), it lines up with it whatever scrollbar the window has, and on a
   desktop its foot follows the menu's rounded corners. A browser without
   scroll timelines simply goes without the fade. */
@supports (timeline-scope: --a) and (animation-timeline: scroll()) {
  .hdr { timeline-scope: --hm-scroll; }
  .hdr-menu { scroll-timeline: --hm-scroll block; }
  body.hdr-menu-open .hdr::after {
    content: ""; position: absolute; z-index: calc(var(--z-nav) + 1); pointer-events: none;
    top: calc(100% + 6px + var(--hm-max, 100svh - var(--hdr-h) - 24px) - var(--hm-foot) - 40px); height: 40px;
    inset-inline-end: calc(max(var(--gutter, 16px), (100% - var(--max, 1240px)) / 2) + 1px);
    width: calc(var(--hm-w, min(380px, 100vw - 2 * var(--gutter, 16px))) - 2px);   /* --hm-w: the menu's width, shell.css */
    border-end-start-radius: var(--hm-foot-r); border-end-end-radius: var(--hm-foot-r);
    background: linear-gradient(to bottom, transparent, var(--bg-elev-2) 85%);
    opacity: 0; animation: hm-more linear both; animation-timeline: --hm-scroll; animation-range: calc(100% - 40px) 100%;
  }
  @media (max-width: 600px) { body.hdr-menu-open .hdr::after { inset-inline-start: calc(var(--gutter, 16px) + 1px); width: auto; } }
}
@keyframes hm-more { from { opacity: 1; } to { opacity: 0; } }
@media (forced-colors: active) { .sound-panel__range::-webkit-slider-runnable-track { background: CanvasText; } .sound-panel__range::-webkit-slider-thumb { background: Highlight; border-color: Canvas; } }
/* The pop-up is faceted like every other stone (the diagonal drawn by
   --facet-edges, or the outline broke at two corners), and it lies above the
   message layer: the stack steps up out of its way (below), so nothing of
   either is covered. */
.sound-panel--pop { --sp-ground: var(--bg-elev-2); --bd: var(--line-strong); --facet: 8px; position: fixed; bottom: calc(var(--s-6) + 44px + var(--s-2)); inset-inline-end: var(--s-6); z-index: calc(var(--z-raven) + 1); width: 248px; padding: var(--s-3) var(--s-4); background: var(--facet-edges), var(--bg-elev-2); border: 1px solid var(--bd); clip-path: var(--clip-facet); box-shadow: var(--shadow-stone); animation: hx-logrow .2s var(--ease-out) both; }
.sound-panel--pop[hidden] { display: none; }
/* With a message up, the two moves take turns: the stack lifts first (.24s,
   below) and the pop-up rises once the lowest message has cleared its box. Both
   used to start on one frame, and for three frames the half-clear player and
   the climbing message read through each other. By 150ms the lift is 99% done
   (ease-out), the message's foot already 6px above the pop-up's top. With no
   message there is nothing to wait for; with motion off neither moves (base.css
   zeroes the delay). */
body:has(.raven-msg > *) .sound-panel--pop { animation-delay: 150ms; }
/* While the pop-up is open, the message stack (same corner, just above the pill)
   lifts by the pop-up's height (sound.js measures it into --sound-pop-h). */
.raven-msg { transition: translate .24s var(--ease-out); }
body.has-sound-pop .raven-msg { translate: 0 calc(-1 * (var(--sound-pop-h, 180px) + var(--s-2))); }
:root[data-motion="off"] .raven-msg { transition: none; }
