/* ═══ HELHEIMRS — shell layout: header, bottom nav, footer, overlays ═══ */

/* ── Header ──
   One emerald thing at rest: the primary account action (or the reader's own
   face), plus the underline that says which realm you are in. Search is a
   quiet field; theme, language and the bell are borderless glyphs that light
   only when you reach them. Every control is one height, --ctl-h. */
.hdr { position: sticky; top: 0; z-index: var(--z-nav); background: linear-gradient(180deg, rgba(10,10,11,.98), rgba(10,10,11,.95)); border-bottom: 1px solid var(--line); padding-top: var(--safe-t); }
/* the iPhone app's clock is white over the status bar (black-translucent): in
   Frost the band under it stays dark, so the clock is still read */
:root[data-theme="light"] .hdr { background: linear-gradient(180deg, #1F2220 var(--safe-t), rgba(227,230,224,.98) var(--safe-t), rgba(227,230,224,.95)); }
.hdr__in { display: flex; align-items: center; gap: var(--s-5); min-height: var(--hdr-in); }
/* the installed app: nothing under a landscape notch, and no pull past the page's ends */
body { padding-left: env(safe-area-inset-left); padding-right: env(safe-area-inset-right); }
@media (display-mode: standalone) { html, body { overscroll-behavior-y: none; } }
.hdr__logo { display: flex; align-items: center; gap: var(--s-3); color: var(--fg-strong); text-decoration: none; flex: none; }
.hdr__logo:hover { text-decoration: none; }
.hdr__logo svg { height: 42px; width: auto; }
.hdr__logo .wordmark { height: 16px; width: auto; }
/* the nav fills the bar's height, so the current realm's underline lands on
   the header's own rule instead of hanging 12px above it */
.hdr__nav { display: flex; gap: var(--s-1); flex: 0 0 auto; align-self: stretch; }
.hdr__nav a { position: relative; display: flex; align-items: center; padding: 0 var(--s-3); color: var(--fg-muted); font-weight: 600; font-size: var(--fs-sm); line-height: var(--lh-ui); transition: color var(--t-fast) var(--ease-stone); white-space: nowrap; }
.hdr__nav a:hover { color: var(--fg-strong); text-decoration: none; }
.hdr__nav a:focus-visible { outline-offset: -6px; }
.hdr__nav a[aria-current="page"] { color: var(--accent-text); }
.hdr__nav a[aria-current="page"]::after { content: ""; position: absolute; inset-inline: var(--s-3); bottom: -1px; height: 2px; background: var(--emerald); }
:root[data-theme="light"] .hdr__nav a[aria-current="page"]::after { background: var(--jade); }
/* The nav keeps its natural width and the tools take what is left, giving way
   from the inside: with the nav as the flexible one (flex: 1, min-width: 0) it
   was the nav that shrank, and «شخصیت‌ها» slid under the search field and the
   theme switch on every 1093–1180px laptop (a 1366 or 1440 screen at 125%). */
.hdr__tools { display: flex; align-items: center; gap: var(--s-2); flex: 1 1 auto; min-width: 0; flex-wrap: nowrap; justify-content: flex-end; }
/* one height for every control in the bar, so their edges and chamfers share
   two lines (shell.css styles the quiet tools themselves) */
.hdr__tools > :is(.btn, .sound-btn) { min-height: var(--ctl-h); height: var(--ctl-h); --facet: 6px; }
.hdr__tools > .btn--icon { width: var(--ctl-h); }
:where(.hdr__tools) .ico { --n: 20; }
/* the quiet tools and the search field drop the chamfer (shell.css): a shape
   with no cut must not draw the cut's edge either, so their facet is zero */
.hdr :is(.hdr__tool, .hdr__search) { --facet: 0px; }
.hdr .kbd-hint { display: none; }
@media (min-width: 1340px) { .hdr .kbd-hint { display: inline-flex; gap: 3px; align-items: center; margin-inline-start: auto; color: var(--fg-faint); font-size: var(--fs-xs); direction: ltr; unicode-bidi: isolate; } }
@media (max-width: 56.25em) { .hdr__nav { display: none; } .hdr__in { justify-content: space-between; gap: var(--s-3); } }
@media (max-width: 37.5em) { .hdr__logo .wordmark { display: none; } .hdr__tools > .btn--primary.btn--sm { padding: 0 var(--s-3); } }
.hdr__user { display: flex; align-items: center; gap: var(--s-2); min-height: var(--ctl-h); color: var(--fg); min-width: 0; }
.hdr__user:hover { text-decoration: none; }
/* the name beside the face. A display name can be long and the header has
   buttons to fit, so it is cut rather than allowed to push them, and it
   steps aside entirely where the nav already has. It is laid out in its own
   script's direction, so a Latin name keeps its first word and loses its end. */
.hdr__user-name { max-width: 110px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: var(--fs-sm); }
.hdr__user:hover .hdr__user-name { color: var(--accent-text); }

/* ── Bottom nav (mobile) ── */
/* The ground is opaque. At 94–96% the page scrolling under it came through
   crisp, a second line of text behind «خانه» and «جلسه‌ها», which reads as a
   rendering fault, and an unblurred ghost costs the labels contrast exactly
   where they are read. A blur would hide it too, but would repaint on every
   scroll frame of a 500-post feed on a cheap phone. */
.bnav { display: none; position: fixed; inset-inline: 0; bottom: 0; z-index: var(--z-nav); background: linear-gradient(180deg, var(--basalt), var(--void)); border-top: 1px solid var(--line-strong); padding: 0 env(safe-area-inset-right) env(safe-area-inset-bottom) env(safe-area-inset-left); }
:root[data-theme="light"] .bnav { background: linear-gradient(180deg, #FBFBF9, #E3E6E0); }
.bnav__in { display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); align-items: end; height: var(--bnav-h); }
.bnav a, .bnav button { display: grid; justify-items: center; gap: 2px; min-width: 0; padding: 8px 2px; color: var(--fg-muted); font-size: var(--fs-2xs); font-weight: 600; line-height: 1.3; text-decoration: none; background: none; border: 0; }
.bnav a span { max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.bnav a[aria-current="page"] { color: var(--accent-text); }
:where(.bnav) .ico { --n: 24; }
/* the raised d20 is raised by a transform only: its own box stays inside the
   bar's 64px row. With the buttons' padding it was 76px tall, the row grew to
   fit it, and every label in the bar sat 12px below the bottom of the screen. */
.bnav .bnav__d20 { padding: 0; height: var(--bnav-h); align-self: end; place-items: center; }
.bnav__d20 { position: relative; transform: translateY(-16px); filter: drop-shadow(0 0 10px rgba(47,211,154,.65)) drop-shadow(0 0 28px rgba(47,211,154,.3)); }
.bnav__d20 span { display: grid; place-items: center; width: 60px; height: 60px; clip-path: var(--clip-hex); background: var(--emerald-grad); color: var(--accent-fg); font-family: var(--font-display); font-weight: 900; font-size: 1.375rem; animation: breathe 4.2s ease-in-out 3; }
:where(.bnav__d20) .ico { --n: 32; }
@media (max-width: 56.25em) {
  .bnav { display: block; }
  /* the page ends above the bar, with room for the raised d20 */
  body { padding-bottom: calc(var(--bnav-h) + var(--s-5) + env(safe-area-inset-bottom)); }
}
/* the d20 is the single resting beacon — and even it settles: three slow
   breaths, then still (fx may re-arm it after a long idle) */
@keyframes breathe { 0%, 100% { opacity: .82; transform: scale(.97); } 50% { opacity: 1; transform: scale(1); } }

/* ── Footer ── */
.ftr { border-top: 1px solid var(--line); margin-top: var(--s-8); padding: var(--s-7) 0 var(--s-6); color: var(--fg-muted); font-size: var(--fs-sm); position: relative; overflow: hidden; }
/* the sign-in pages are a single gate; the footer would only pull the eye away */
:root:is([data-page="login"], [data-page="signup"], [data-page="forgot"], [data-page="reset"], [data-page="verify"], [data-page="notfound"]) .ftr { display: none; }
/* five blocks, five columns: the brand and four lists on one row */
.ftr__in { display: grid; grid-template-columns: minmax(0, 1.3fr) repeat(4, minmax(0, 1fr)); gap: var(--s-6); }
.ftr__in > * { min-width: 0; }
.ftr .ftr__h { color: var(--fg-strong); font-size: var(--fs-md); font-weight: 700; margin: 0 0 var(--s-3); }
.ftr ul { list-style: none; padding: 0; margin: 0; display: grid; gap: var(--s-half); }
/* A link inside a sentence cannot be marked by colour alone: the credits line
   is one sentence with one link in it, and the nav lists above are not. */
.ftr a { color: var(--fg-muted); }
.ftr__bottom a { text-decoration: underline; text-underline-offset: 2px; }
.ftr a:hover { color: var(--accent-text); }
/* the lockup is never a 346px banner of type: whatever svg the brand block
   holds is sized by its own rule (shell.css), never stretched to the column */
.ftr__brand svg { width: auto; max-width: 100%; }
.ftr__brand p { font-size: var(--fs-sm); line-height: 1.8; max-width: 36ch; }
.ftr__bottom { margin-top: var(--s-6); padding-top: var(--s-4); border-top: 1px solid var(--line); display: flex; justify-content: space-between; gap: var(--s-2) var(--s-4); flex-wrap: wrap; font-size: var(--fs-xs); line-height: 1.7; color: var(--fg-faint); }
.ftr .rune-band { margin-top: var(--s-5); }
@media (max-width: 68.75em) { .ftr__in { grid-template-columns: repeat(4, minmax(0, 1fr)); } .ftr__brand { grid-column: 1 / -1; } }
@media (max-width: 56.25em) { .ftr__in { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (max-width: 32.5em) { .ftr__in { grid-template-columns: 1fr; } }
/* on a desktop the last lines of the page scroll clear of the sound pill */
@media (min-width: 56.3em) { .ftr { padding-bottom: calc(var(--s-6) + var(--fab)); } }

/* ── The d20 (desktop) ──
   A tool in the header's row, beside search: the one tool in the bar drawn in
   emerald, as the d20 is the site's own die. It used to float, 64px, in the
   content's bottom corner, where at rest it sat on the first letters of a
   lead headline, a session row's title, a stepper, a focused glossary word.
   The tray still grows out of it (dice.js reads where it is). On a phone the
   bottom bar's raised d20 is the die, and this one is not shown. */
.dice-fab {
  position: relative; display: grid; place-items: center; flex: none;
  width: var(--ctl-h); height: var(--ctl-h); padding: 0; border: 1px solid transparent; border-radius: var(--r-1);
  background: none; color: var(--accent-text); cursor: pointer;
  transition: background var(--t-fast) var(--ease-stone), color var(--t-fast) var(--ease-stone);
}
.dice-fab:hover { background: rgba(47,211,154,.1); color: var(--emerald-soft, var(--accent-text)); }
.dice-fab:focus-visible { border-color: var(--line-strong); }
:root[data-theme="light"] .dice-fab:hover { background: rgba(15,107,79,.08); color: var(--accent-text); }
.dice-fab svg { position: relative; animation: breathe 4.2s ease-in-out 3; }
:where(.dice-fab) .ico { --n: 24; }
/* only ever in the bar: if the shell could not seat it there, it is not shown loose on the page */
body > .dice-fab { display: none; }
@media (max-width: 56.25em) { .dice-fab { display: none; } }
/* while a field is being typed in, the floating sound pill steps out of its way */
body:has(#app :is(input, textarea, select):focus) > .sound-btn { opacity: 0; pointer-events: none; }
/* an operator does not need the music pill over the console's primary buttons */
:root[data-page="admin"] body > .sound-btn { display: none; }

/* ── Sound toggle ──
   The switch lives in the header menu («نمایش»), for everyone, on every
   screen. The floating pill is the visualiser: it appears on a desktop only
   while the music is playing — a live control with something to show, and
   one-click mute — instead of sitting over the content corner of every page
   for a feature most readers never turn on. On a phone it never floats: there
   it covered the end of a sentence on every page. */
.sound-btn { --bd: var(--line-strong); --facet: 8px; display: flex; align-items: center; gap: var(--s-2); padding: 8px 12px; background: var(--facet-edges), var(--bg-elev); border: 1px solid var(--bd); clip-path: var(--clip-facet); color: var(--fg-muted); font-size: var(--fs-xs); font-weight: 600; line-height: 1; cursor: pointer; transition: color var(--t-fast) var(--ease-stone), border-color var(--t-fast) var(--ease-stone), opacity var(--t-fast) var(--ease-stone); }
.sound-btn:hover { color: var(--fg-strong); --bd: var(--control-edge); }
.sound-btn[aria-pressed="true"] { color: var(--accent-text); --bd: var(--line-glow); }
:where(.sound-btn) .ico { --n: 20; }
/* five rune staves, 20px tall and legible at rest; the branches answer the sound */
.sound-btn .viz { display: flex; align-items: center; direction: ltr; }
.sound-btn .viz__svg { width: auto; height: 20px; }
.sound-btn[aria-pressed="false"] .viz { opacity: .9; }
body > .sound-btn { position: fixed; bottom: var(--s-6); inset-inline-end: var(--s-6); z-index: var(--z-tray); }
body > .sound-btn:not([aria-pressed="true"]) { display: none; }
@media (max-width: 56.25em) { body > .sound-btn { display: none; } }
/* if the shell ever seats it in the header: a quiet control the height of its neighbours */
.hdr__tools > .sound-btn { position: static; --bd: transparent; --facet: 0px; background: transparent; padding: 0 var(--s-2); clip-path: none; }
.hdr__tools > .sound-btn .sound-btn__label { display: none; }
.hdr__tools > .sound-btn .viz__svg { height: 16px; }

/* ── Gatekeeper guide (corner mascot) ──
   Desktop: a small card in the bottom start corner. Phone: a slim sheet docked on
   the bottom bar, the width of the screen, never a card floating over text. */
.guide { position: fixed; bottom: var(--s-6); inset-inline-start: var(--s-6); z-index: var(--z-tray); display: flex; align-items: flex-end; gap: var(--s-3); max-width: 340px; pointer-events: none; opacity: 0; transform: translateY(12px); transition: opacity var(--t-slow) var(--ease-out), transform var(--t-slow) var(--ease-out); }
.guide.is-on { opacity: 1; transform: none; pointer-events: auto; }
.guide .keeper { width: 64px; flex: none; filter: drop-shadow(0 0 1px rgba(123,230,192,.8)) drop-shadow(0 0 12px rgba(47,211,154,.4)); }
:root[data-theme="light"] .guide .keeper { filter: none; }
.guide .speech { margin-bottom: 8px; max-height: 140px; overflow: auto; }
.guide .speech button { margin-top: 6px; }
/* On a phone the sheet is the frame — the keeper and the words on one stone —
   so no line of the page shows through between them. */
@media (max-width: 56.25em) {
  .guide {
    --bd: var(--line-glow); --facet: 8px;
    inset-inline: var(--s-2); max-width: none; align-items: center;
    /* clear of the d20 that rises out of the bottom bar */
    bottom: calc(var(--bnav-h) + env(safe-area-inset-bottom) + var(--s-5));
    gap: var(--s-2); padding: var(--s-2) var(--s-2) var(--s-2) var(--s-3);
    background: var(--facet-edges), var(--bg-elev-2); border: 1px solid var(--bd); clip-path: var(--clip-facet); box-shadow: var(--glow-inset-soft);
  }
  .guide .keeper { width: 40px; }
  .guide .speech { flex: 1; max-width: none; max-height: 96px; margin: 0; padding: 0 var(--s-1); background: none; border: 0; clip-path: none; --facet: 0px; box-shadow: none; }
}

/* ── Fog canvas & lantern ── */
/* width/height are required, not decorative: fog.js renders the backing store at
   half resolution (SCALE .5) and relies on the element stretching to the viewport.
   Without these the canvas takes its intrinsic half size and, in this RTL document,
   `inset: 0` parks it at the TOP-RIGHT — a hard-edged rectangle of fog over the
   corner of every page.
   The fog is atmosphere, so it lives BEHIND the words (z-index -1: under every
   card and line of text, over the bare ground only), and it stays out of the
   reading column, where it lifted the void to the value of the cards. It is
   thick only on the pages that are all atmosphere. */
.fog-canvas { position: fixed; inset: 0; width: 100%; height: 100%; z-index: -1; pointer-events: none; opacity: var(--fog-opacity);
  -webkit-mask-image: linear-gradient(90deg, #000 0, rgba(0,0,0,.3) 20%, rgba(0,0,0,.3) 80%, #000 100%);
  mask-image: linear-gradient(90deg, #000 0, rgba(0,0,0,.3) 20%, rgba(0,0,0,.3) 80%, #000 100%); }
:root:is([data-page="home"], [data-page="notfound"], [data-page="niflheim"], [data-page="dust"]) .fog-canvas { opacity: .85;
  -webkit-mask-image: linear-gradient(90deg, #000 0, rgba(0,0,0,.5) 22%, rgba(0,0,0,.5) 78%, #000 100%);
  mask-image: linear-gradient(90deg, #000 0, rgba(0,0,0,.5) 22%, rgba(0,0,0,.5) 78%, #000 100%); }
:root[data-theme="light"] .fog-canvas { mix-blend-mode: multiply; }
:root[data-theme="light"]:is([data-page="home"], [data-page="notfound"], [data-page="niflheim"], [data-page="dust"]) .fog-canvas { opacity: .3; }
/* the lantern: shown in Lantern mode (the arrow hidden) and in Lantern-with-arrow mode */
.lantern { position: fixed; left: 0; top: 0; z-index: var(--z-cursor); pointer-events: none; width: 26px; height: 26px; margin: -13px 0 0 -13px; border-radius: 50%; border: 1.5px solid rgba(47,211,154,.9); box-shadow: 0 0 10px rgba(47,211,154,.6), 0 0 60px 18px rgba(47,211,154,.12); mix-blend-mode: screen; transition: transform var(--t-fast) var(--ease-out), opacity var(--t-fast) var(--ease-out); opacity: 0; }
.lantern::after { content: ""; position: absolute; inset: 9px; border-radius: 50%; background: var(--emerald); box-shadow: 0 0 8px var(--emerald); }
/* a separate exact-position dot, when fx draws one, replaces the ring's centre */
.lantern-dot { position: fixed; left: 0; top: 0; z-index: var(--z-cursor); pointer-events: none; width: 6px; height: 6px; margin: -3px 0 0 -3px; border-radius: 50%; background: var(--emerald); box-shadow: 0 0 8px var(--emerald); opacity: 0; transition: opacity var(--t-fast) var(--ease-out); }
body:has(.lantern-dot) .lantern::after { display: none; }
body:is(.has-lantern, .cursor-both) :is(.lantern, .lantern-dot).is-live { opacity: 1; }
/* over something you can press, the real pointer is shown (base.css) and the
   light steps back, so it never sits on the label you are about to click */
.lantern.is-hover { transform: scale(.7); opacity: .2 !important; }
.lantern.is-down { transform: scale(.6); }
/* over a glossary word or its definition, a native list, or the search, the light goes out */
body:has(.term:hover, .tip:hover, .palette:hover, select:focus) :is(.lantern, .lantern-dot) { opacity: 0 !important; }
:root[data-theme="light"] .lantern { mix-blend-mode: multiply; border-color: rgba(15,107,79,.8); box-shadow: 0 0 0 1px rgba(15,107,79,.15); }
:root[data-theme="light"] .lantern::after, :root[data-theme="light"] .lantern-dot { background: var(--jade); box-shadow: none; }
/* hidden runes revealed by the lantern */
.secret-rune { position: absolute; font-family: var(--font-rune); font-size: 1.75rem; color: var(--emerald); opacity: 0; pointer-events: none; user-select: none; transition: opacity .3s var(--ease-out); text-shadow: 0 0 12px var(--emerald); }
.secret-msg { position: absolute; font-size: var(--fs-xs); color: var(--emerald-soft); opacity: 0; pointer-events: none; white-space: nowrap; transition: opacity .3s var(--ease-out); }
.secret-rune.is-lit, .secret-msg.is-lit { opacity: .9; }

/* ── Portal page transition ──
   fx/portal.js draws the gate as one SVG inside this layer (fx.css). The veil
   is the reader's own ground — never a black or emerald slab. */
.portal-x { position: fixed; inset: 0; z-index: var(--z-portal); pointer-events: none; display: grid; place-items: center; opacity: 0; }
.portal-x.is-on { pointer-events: auto; opacity: 1; }
.portal-x__veil { position: absolute; inset: 0; background: var(--bg); opacity: 0; }
.portal-x__frame { position: absolute; width: 180px; height: 220px; clip-path: var(--clip-hex); background: transparent; box-shadow: inset 0 0 0 2px var(--emerald), inset 0 0 18px rgba(47,211,154,.35); transform: scale(0); }
.portal-x__rune { position: absolute; font-family: var(--font-rune); font-size: 2.625rem; color: var(--emerald); text-shadow: 0 0 20px var(--emerald); opacity: 0; }

/* ── Ravens ── */
.raven-layer { position: fixed; inset: 0; z-index: var(--z-raven); pointer-events: none; overflow: hidden; }
.raven { position: absolute; left: 0; top: 0; width: 132px; height: 132px; will-change: transform; }
/* left/top pinned on purpose: the flight path in fx/ravens.js is computed in
   viewport coordinates, and an auto-offset absolute box takes its static
   position — the RIGHT edge in this RTL document — which flung the bird
   off-screen by the width of the viewport. */
.raven svg { filter: drop-shadow(0 0 12px rgba(47,211,154,.34)) drop-shadow(0 2px 6px rgba(0,0,0,.5)); }
/* the raven's note: under the header, lifted off the page by its shell */
.raven-msg { position: fixed; z-index: var(--z-raven); top: calc(var(--hdr-h) + var(--s-3)); inset-inline-start: var(--s-5); width: min(380px, calc(100vw - 2 * var(--s-3))); pointer-events: auto; filter: var(--lift); }
.raven-msg .toast { transform-origin: top; }
@media (max-width: 56.25em) { .raven-msg { inset-inline: var(--s-3); width: auto; top: calc(var(--hdr-h) + var(--s-2)); } }

/* ── Fumble screen effect (the crit itself plays on the dice tray, fx/dice.js) ── */
/* the colour drains in and comes back — a fade, not a switch */
body.is-fumble::before { content: ""; position: fixed; inset: 0; z-index: calc(var(--z-portal) - 1); pointer-events: none; backdrop-filter: grayscale(1) contrast(.9); -webkit-backdrop-filter: grayscale(1) contrast(.9); animation: fumble-grey 1.8s linear both; }
@keyframes fumble-grey { 0% { opacity: 0; } 10% { opacity: 1; } 72% { opacity: 1; } 100% { opacity: 0; } }
.fumble-crack { position: fixed; inset: 0; z-index: var(--z-portal); pointer-events: none; opacity: 0; animation: crackIn 2.2s var(--ease-out) both; }
@keyframes crackIn { 0% { opacity: 0 } 10% { opacity: 1 } 80% { opacity: 1 } 100% { opacity: 0 } }
.ghost-hand { position: fixed; bottom: 0; left: 50%; transform: translate(-50%, 100%); z-index: var(--z-portal); pointer-events: none; width: 220px; opacity: .85; animation: handUp 2.4s var(--ease-heavy) both; filter: drop-shadow(0 0 20px rgba(200,220,210,.4)); }
@keyframes handUp { 0% { transform: translate(-50%, 100%) } 35% { transform: translate(-50%, 18%) } 65% { transform: translate(-50%, 22%) } 100% { transform: translate(-50%, 100%) } }

/* ── Ragnarök (Konami) ── */
.ragnarok { position: fixed; inset: 0; z-index: var(--z-portal); pointer-events: none; }
/* The scene itself — sky, burning tree, sea, embers — is drawn by eggs.js and
   styled in fx.css; no photograph sits under it any more. Ember is the reserved
   danger colour and this is the end of the world — the one place it gets a
   whole screen. The cold emerald serpent rises through it. */
.ragnarok__serpent { position: absolute; inset: 0; width: 100%; height: 100%; filter: drop-shadow(0 0 30px rgba(47,211,154,.45)); animation: rise 1.8s var(--ease-out) both; }
/* the title slams in at 900 ms and STAYS: its last frame is the readable one,
   so reduced motion (which jumps to the last frame) shows it too */
.ragnarok__title { position: absolute; top: 18%; left: 0; right: 0; text-align: center; font-family: var(--font-display); font-weight: 900; font-size: clamp(2.4rem, 8vw, 6rem); color: var(--emerald); text-shadow: 0 0 30px var(--emerald); animation: rag-slam 420ms var(--ease-spring) 900ms both; }
@keyframes rag-slam { from { opacity: 0; transform: scale(1.3); } to { opacity: 1; transform: none; } }

/* ── Dice tray ── */
.tray-backdrop { position: fixed; inset: 0; z-index: var(--z-tray); background: rgba(0,0,0,.4); backdrop-filter: blur(3px); animation: fade var(--t-ui) var(--ease-out) both; }
/* the tray grows out of the d20 it was opened from, and folds back into it */
/* desktop: it drops from the header's d20, under the bar at the tools' end */
.tray { --bd: var(--line-strong); --facet: 14px; max-height: calc(100vh - var(--hdr-h) - 24px); max-height: calc(100dvh - var(--hdr-h) - 24px); overflow-y: auto; overscroll-behavior: contain; position: fixed; z-index: calc(var(--z-tray) + 1); top: calc(var(--hdr-h) + var(--s-2)); inset-inline-end: max(var(--gutter), calc((100vw - var(--max)) / 2)); width: min(440px, calc(100vw - 2 * var(--s-4))); background: var(--facet-edges), var(--stone-grad); border: 1px solid var(--bd); clip-path: var(--clip-facet); box-shadow: var(--shadow-stone), var(--glow-inset);
  transform-origin: top; animation: tray-open 280ms cubic-bezier(.2,.9,.3,1.1) both; }
.tray.is-closing { animation: tray-close 180ms var(--ease-exit) both; }
@keyframes tray-open { from { opacity: 0; transform: scale(.4); } to { opacity: 1; transform: none; } }
@keyframes tray-close { from { opacity: 1; transform: none; } to { opacity: 0; transform: scale(.4); } }
@media (max-width: 56.25em) { .tray { top: auto; max-height: calc(100dvh - var(--bnav-h) - var(--hdr-h) - 32px); bottom: calc(var(--bnav-h) + 20px + env(safe-area-inset-bottom)); inset-inline: var(--s-3); width: auto; transform-origin: 50% calc(100% + 40px); } }
.tray__head { display: flex; align-items: center; justify-content: space-between; padding: var(--s-3) var(--s-4); border-bottom: 1px solid var(--line); }
.tray__head :is(h2,h3) { margin: 0; font-size: var(--fs-md); }
.tray__slab { position: relative; height: 240px; background: radial-gradient(ellipse at 50% 40%, #232527, #111214 80%); border-block: 1px solid var(--line); overflow: hidden; }
.tray__slab canvas { width: 100%; height: 100%; }
.tray__slab::before { content: "ᚠᚢᚦᚨᚱᚲ ᚷᚹᚺᚾᛁᛃ ᛈᛉᛊᛏᛒᛖ ᛗᛚᛜᛞᛟ"; position: absolute; inset: 0; display: grid; place-items: center; font-family: var(--font-rune); color: rgba(255,255,255,.04); font-size: 2.25rem; letter-spacing: .5em; text-align: center; pointer-events: none; line-height: 1.6; }
.tray__result { position: absolute; bottom: var(--s-3); inset-inline: var(--s-4); display: flex; align-items: baseline; gap: var(--s-3); font-variant-numeric: tabular-nums; }
.tray__result .total { font-family: var(--font-display); font-weight: 900; font-size: 2.75rem; line-height: 1; color: var(--emerald); text-shadow: 0 0 16px rgba(47,211,154,.7); }
.tray__result .total.is-nat1 { color: var(--ember-soft); text-shadow: 0 0 16px rgba(194,58,43,.7); }
.tray__result .detail { color: var(--fg-muted); font-size: var(--fs-sm); direction: ltr; unicode-bidi: isolate; font-family: var(--font-mono); }
.tray__ctrl { padding: var(--s-3) var(--s-4); display: grid; gap: var(--s-3); }
.tray__dice { display: grid; grid-template-columns: repeat(7, minmax(0, 1fr)); gap: var(--s-half); }
.tray__dice button { min-height: 40px; padding: 0; font-family: var(--font-display); font-weight: 800; font-size: var(--fs-xs); }
.tray__dice button.is-sel { color: var(--accent-text); --bd: var(--line-glow); box-shadow: var(--glow-inset); }
.tray__expr { display: flex; gap: var(--s-2); }
/* the hand: one chip per die type with its own − and +, then the flat modifier */
.tray__pool { display: flex; flex-wrap: wrap; align-items: center; gap: var(--s-2); min-height: 36px; }
.tray__empty { color: var(--fg-faint); font-size: var(--fs-xs); }
.tray__chip { --bd: var(--line-glow); --facet: 6px; display: inline-flex; align-items: stretch; border: 1px solid var(--bd); background: var(--facet-edges), var(--bg-elev-2); clip-path: var(--clip-facet); }
.tray__chip b { display: flex; align-items: baseline; justify-content: center; align-self: center; gap: 3px; min-width: 3.6em; padding-inline: 6px; font-weight: 800; font-size: var(--fs-sm); color: var(--fg); font-variant-numeric: tabular-nums; }
.tray__chip b .die { font-family: var(--font-mono); font-weight: 600; color: var(--accent-text); }
.tray__chip.is-neg b { color: var(--ember-soft); }
.tray__chip--mod { --bd: var(--line-strong); }
.tray__chip--mod b { min-width: 2.8em; color: var(--fg-muted); }
.tray__chip button { min-width: 36px; min-height: 36px; border: 0; background: none; color: var(--accent-text); font-size: 1.125rem; line-height: 1; cursor: pointer; }
.tray__chip button:hover { background: rgba(47,211,154,.1); }
.tray__chip button:focus-visible { outline: 2px solid var(--emerald); outline-offset: -2px; }
.tray__clear { margin-inline-start: auto; min-height: 36px; padding-inline: var(--s-3); border: 1px solid var(--line); background: none; color: var(--fg-muted); font: inherit; font-size: var(--fs-xs); cursor: pointer; }
.tray__clear:hover { color: var(--fg); border-color: var(--line-strong); }
/* the tray is the one panel built entirely out of small controls, and it is used
   with a thumb more than with a mouse: +/−, the die buttons and Clear all reach
   the 44px target where the pointer is coarse */
@media (pointer: coarse) {
  .tray__dice button { min-height: 44px; }
  .tray__chip button { min-width: 44px; min-height: 44px; }
  .tray__pool { min-height: 44px; }
  .tray__clear { min-height: 44px; }
  .tray__legend summary { min-height: 44px; }
}
.tray__read { display: grid; gap: 2px; min-width: 0; }
.tray__result .runes { display: flex; flex-wrap: wrap; align-items: center; gap: .32em; font-family: var(--font-rune); font-size: 1.25rem; line-height: 1.1; color: var(--emerald-soft); direction: ltr; text-shadow: 0 0 10px rgba(47,211,154,.45); }
.tray__result .runes .gate { width: .78em; height: .95em; filter: drop-shadow(0 0 5px rgba(47,211,154,.5)); }
.tray__legend { font-size: var(--fs-xs); color: var(--fg-muted); }
.tray__legend summary { cursor: pointer; color: var(--accent-text); min-height: 32px; display: flex; align-items: center; }
.tray__legend p { margin: var(--s-2) 0; line-height: 1.8; }
.tray__runes { list-style: none; margin: 0; padding: 0; display: grid; grid-template-columns: repeat(10, minmax(0, 1fr)); gap: 4px; }
.tray__runes li { display: grid; justify-items: center; gap: 1px; padding: 3px 0; border: 1px solid var(--line); }
.tray__runes b { font-family: var(--font-rune); font-weight: 400; font-size: 1.0625rem; color: var(--fg); }
@media (max-width: 26.25em) { .tray__runes { grid-template-columns: repeat(7, minmax(0, 1fr)); } }
/* dice notation must be read exactly — Cinzel sets lowercase as small caps and its
   1 is a bare stem, so "1d8" read as "ID8". A monospace keeps every digit distinct. */
.tray__expr .input { direction: ltr; text-align: left; font-family: var(--font-mono); font-weight: 600; letter-spacing: .02em; min-height: 42px; }
/* the log keeps its height from the first roll, so the slab — and the die on
   it — does not jump up a row on the very frame the result appears */
.tray__log { min-height: 96px; max-height: 96px; overflow: auto; font-size: var(--fs-xs); color: var(--fg-muted); display: grid; gap: 2px; align-content: start; }
.tray__log > * { animation: hx-log-in 200ms var(--ease-out) both; }
@keyframes hx-log-in { from { opacity: 0; transform: translateY(-4px); } to { opacity: 1; transform: none; } }
.tray__log b { color: var(--accent-text); }

/* ── Working: a page drawn again in place keeps showing until the new one is
   ready; past 400 ms this hairline under the header says it is on its way
   (core.js). A sliver of light crossing, never a loader over the page. */
.hx-progress { position: fixed; z-index: calc(var(--z-nav) + 1); top: var(--hdr-h); inset-inline: 0; height: 2px; pointer-events: none; opacity: 0; transition: opacity var(--t-fast) var(--ease-out);
  background: linear-gradient(90deg, transparent, var(--emerald) 50%, transparent) 0 0 / 40% 100% no-repeat; }
.hx-progress.is-on { opacity: 1; animation: hx-progress 1.1s linear infinite; }
@keyframes hx-progress { from { background-position: -70% 0; } to { background-position: 170% 0; } }
:root[data-theme="light"] .hx-progress { background-image: linear-gradient(90deg, transparent, var(--jade) 50%, transparent); }
/* without motion it is a still line, half lit */
:root[data-motion="off"] .hx-progress.is-on { animation: none; background: var(--emerald); opacity: .5; }
@media (prefers-reduced-motion: reduce) { .hx-progress.is-on { animation: none; background: var(--emerald); opacity: .5; } }
#app[aria-busy="true"] { cursor: progress; }

/* ── Nothing decorative may make the page longer ──
   The lantern's hidden runes are placed for the page as it was drawn; a filter
   or a search shortens the page under them, and a rune left at y≈2,800 held
   the document open — a screen and a half of black under the footer after the
   hall's commonest click. #app clips what hangs outside it, with a margin so a
   menu or a date picker opened at the end of a short page may still reach
   over the footer (fx.css), as it is meant to; the margin is less than the
   footer is tall, so whatever it lets through never reaches past the footer's
   end. overflow: clip is not a scroll container: sticky bars inside still stick. */
#app { overflow: clip; overflow-clip-margin: 320px; }
:root:is([data-page="login"], [data-page="signup"], [data-page="forgot"], [data-page="reset"], [data-page="verify"], [data-page="notfound"]) #app { overflow-clip-margin: 0px; }

/* ── Loading: the page holds its height, so the footer never rides up into view ── */
#app:has(> .loading) { min-height: calc(100svh - var(--hdr-h)); }

/* ── Print: the page, not the chrome ── */
@media print {
  .hdr, .bnav, .ftr, .dice-fab, .sound-btn, .toasts, .raven-layer, .raven-msg, .fog-canvas, .lantern, .lantern-dot, .guide, .portal-x, .tray, .tray-backdrop, .skip-link { display: none !important; }
  body::after { display: none; }
  body { padding-bottom: 0 !important; }
}

/* ── Forced colours: the atmosphere steps aside ── */
@media (forced-colors: active) {
  .fog-canvas, .lantern, .lantern-dot, .raven-layer { display: none; }
  body.has-lantern { cursor: auto; }
  .hdr__nav a[aria-current="page"]::after { forced-color-adjust: none; background: Highlight; }
  .hdr__tools > .btn--ghost { border-color: CanvasText; }
}
