/* ═══ HELHEIMRS — base ═══ */
*, *::before, *::after { box-sizing: border-box; }
/* The scroller has to know about the fixed chrome, or a control reached with
   Tab is scrolled to exactly the strip the sticky header (top) or the bottom
   nav and the d20 (bottom) cover. */
/* A page with a sticky bar of its own (the character builder's step bar, a
   session's booking bar) says how tall it is in --page-bar-b (bottom) or
   --page-bar-t (top), set on :root from the page's stylesheet with :has(), and
   the scroller keeps a focused control clear of that bar too (WCAG 2.4.11).
   --scroll-pad-b is that bottom strip, named so :focus below can count it. */
html { --scroll-pad-b: calc(var(--page-bar-b, 0px) + var(--s-5)); -webkit-text-size-adjust: 100%; scroll-behavior: smooth; scroll-padding-block: calc(var(--hdr-h) + var(--page-bar-t, 0px) + var(--s-4)) var(--scroll-pad-b); }
@media (max-width: 56.25em) { html { --scroll-pad-b: calc(var(--bnav-h) + var(--page-bar-b, 0px) + var(--s-6) + env(safe-area-inset-bottom)); scroll-padding-bottom: var(--scroll-pad-b); } }
@media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } }
/* the in-app motion switch reaches <html> itself — `:root[data-motion] *` below
   matches only its descendants, so the page kept gliding with the switch off */
:root[data-motion="off"] { scroll-behavior: auto; }

body {
  margin: 0;
  min-height: 100vh;
  background: var(--bg);
  color: var(--fg);
  font-family: var(--font-body);
  font-size: var(--fs-md);
  line-height: var(--lh-body);
  /* direction comes from <html dir>, which follows the reader's language */
  text-align: start;
  overflow-x: hidden;
  /* A word that is wider than its whole line may break, as a last resort;
     nothing else does. This was `anywhere`, which also counts every word as one
     letter wide when a flex or grid column is sized: any squeezed label was
     then cut mid-letter («نیفل|هایم», «روایت‌شد|ه») and figures were split
     («۷٬۳۵۰٬۰|۰۰»), which is a misreading, not a wrap. `anywhere` is kept for
     what a member typed (a 55-letter word, a pasted URL), on the elements that
     show it (each page's sheet), with min-width: 0 on their columns. */
  overflow-wrap: break-word;
  font-feature-settings: "ss01" 0;
  -webkit-font-smoothing: antialiased;
}

/* ── Cursor modes ──
   Lantern: the spirit lantern IS the pointer. Lantern with arrow: the light
   follows a native arrow. Regular: no lantern class at all. The cursors that
   carry meaning (a hand on a link, a caret in a field) always come back. */
body.has-lantern:not(.cursor-both) { cursor: none; }
body.has-lantern a, body.has-lantern button, body.has-lantern [role="button"], body.has-lantern label,
body.has-lantern summary, body.has-lantern .chip, body.has-lantern .tab { cursor: pointer; }
body.has-lantern input, body.has-lantern textarea, body.has-lantern select, body.has-lantern [contenteditable="true"] { cursor: auto; }
/* a native list or picker is drawn by the system, outside the page: the
   lantern cannot follow the pointer there, so it gives the arrow back */
body.has-lantern:has(select:focus, input:is([type="date"], [type="time"], [type="color"], [type="file"]):focus) { cursor: auto; }

/* Film grain — behind the words, never over them, and STILL. It used to be a
   layer 8% larger than the viewport jerked by percentage translations four
   times a second: on Windows at 125%/150% display scaling those moves land on
   fractional device pixels, and a viewport-sized layer moved that way shows its
   GPU tile seams — thin lines across the whole screen, and a page that shakes.
   An exactly viewport-sized layer that never moves cannot do either; at 5%
   opacity a still grain reads the same as a moving one. */
body::after {
  content: ""; position: fixed; inset: 0; pointer-events: none; z-index: -1;
  opacity: var(--grain-opacity);
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='220' height='220'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='.9' numOctaves='3' stitchTiles='stitch'/><feColorMatrix values='0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 .8 0'/></filter><rect width='100%' height='100%' filter='url(%23n)'/></svg>");
  background-size: 220px 220px;
}

/* Persian display needs more lead than Latin: at 1.25 Estedad Black's gaf and
   kaf strokes touch the descenders of the line above when a title wraps. */
h1, h2, h3, h4 {
  font-family: var(--font-display);
  font-weight: 800;
  line-height: var(--lh-display);
  color: var(--fg-strong);
  margin: 0 0 .5em;
  text-wrap: balance;
}
:root[lang="en"] :is(h1, h2, h3, h4) { letter-spacing: -.01em; }
/* every page's H1 is its largest text; one hero exception */
h1 { font-size: var(--fs-page); font-weight: 900; }
.page-title { font-size: var(--fs-page); }
.hero-title { font-size: var(--fs-hero); }
h2 { font-size: var(--fs-3xl); }
h3 { font-size: var(--fs-xl); }
h4 { font-size: var(--fs-lg); }
p { margin: 0 0 1em; max-width: var(--measure); }
/* no paragraph ends on one stranded word */
p, li, .lede, dd { text-wrap: pretty; }
a { color: var(--accent-text); text-decoration: none; text-underline-offset: .22em; }
a:hover { text-decoration: underline; text-decoration-thickness: 1px; }
/* A link inside a sentence cannot be marked by colour alone (WCAG 1.4.1):
   emerald beside light grey body text is only a hue change. Links that ARE the
   whole line — nav lists, buttons, chips, cards — keep their clean edge. */
:is(.prose, p, td, dd, .notice, .hint, .lede, .check, .empty, .tip, .toast) a:not(.btn, .chip, .tag, .sec-more) {
  text-decoration: underline; text-decoration-thickness: 1px; text-underline-offset: .25em;
  text-decoration-color: color-mix(in srgb, currentColor 55%, transparent);
}
:is(.prose, p, td, dd, .notice, .hint, .lede, .check, .empty, .tip, .toast) a:not(.btn, .chip, .tag, .sec-more):hover { text-decoration-color: currentColor; }
small { font-size: var(--fs-sm); }
strong { color: var(--fg-strong); font-weight: 700; }
/* Neither Vazirmatn nor Estedad has an italic: the browser shears Persian to the
   right, against the direction it is written. Persian marks emphasis with
   weight and colour; italic stays a Latin convention. */
:lang(fa) :is(i, em, cite, var, dfn), :lang(fa):is(i, em, cite, var, dfn) { font-style: normal; }
:lang(fa) em { font-weight: 700; color: var(--fg-strong); }
hr { border: 0; height: 1px; background: linear-gradient(90deg, transparent, var(--line-strong), transparent); margin: var(--s-7) 0; }
img, svg, video, canvas { max-width: 100%; display: block; }
ul, ol { padding-inline-start: 1.4em; }
button, input, select, textarea { font: inherit; color: inherit; }
button { cursor: pointer; }
/* code, keys and tokens are read character by character */
code, kbd, samp, pre { font-family: var(--font-mono); font-size: .8125rem; letter-spacing: 0; font-variant: normal; text-transform: none; font-variant-ligatures: none; }
code, samp { direction: ltr; unicode-bidi: isolate; overflow-wrap: anywhere; }
pre { direction: ltr; text-align: left; overflow-x: auto; }
/* HX.ct puts one after each ZWNJ in text people wrote: a break chance that is
   not in the text (never copied, found or spoken) and that, unlike <wbr>,
   white-space: nowrap switches off, so an ellipsised cell stays on one line */
.wb::after { content: "\200B"; }

/* ── Icons ──
   An <svg class="ico"> has a viewBox and no size, so wherever no rule reached
   it, it stretched to its container (a 300px heart on the public profile, a
   79px globe in the session facts). It has a size of its own and never the
   container's — nor a fraction of it: `max-width: 100%` above squeezed the
   session list's chevron into a 7×16 box.
   There are four sizes, 16 / 20 / 24 / 32 (art.js, the style guide's table),
   and the stroke is DRIVEN by the size: a glyph drawn at 1.8u on a 24u grid
   came out 1.05px thin at 14px and 4.2px heavy at 56px wherever a page sized
   it by hand. A context or a page sets --n (a bare number, one of the four) on
   the icon itself; the stroke follows so the drawn line is 1.5px at 16 and 20,
   1.75px at 24 and 2px at 32: 2.25u, 1.8u, 1.75u, 1.5u. --n is declared on the
   icon, so it never inherits the --n that a rune ring or a tooltip keep for
   themselves. (--ico, the old way to size one, still sizes it.) */
@property --ico-sw { syntax: '<number>'; inherits: false; initial-value: 1.8; }
.ico {
  --n: 20;
  /* registered as a number, so the stroke computes to a plain 2.25 / 1.8 /
     1.75 / 1.5, not to an unresolved calc() */
  --ico-sw: calc(min(2, 1.5 + max(0, var(--n) - 20) / 16) * 24 / var(--n));
  width: var(--ico, calc(var(--n) * 1px)); height: var(--ico, calc(var(--n) * 1px));
  max-width: none; flex: none; vertical-align: -.2em;
  stroke-width: var(--ico-sw);
}
.ico.ico--16 { --n: 16; }
.ico.ico--20 { --n: 20; }
.ico.ico--24 { --n: 24; }
.ico.ico--32 { --n: 32; }
/* drawn for RTL; the English page turns the arrows, not the meaning */
[dir="ltr"] .ico--dir { transform: scaleX(-1); }

/* The bottom margin keeps a focused control above the message stack: 140px
   is room for one message; while the stack holds more, core.js says how far up
   the screen it reaches (--toast-h, from the bottom edge) and the margin grows
   to clear it by --s-4, less the strip the scroller already keeps (it adds the two). */
:focus { outline: none; scroll-margin-block: calc(var(--hdr-h) + var(--page-bar-t, 0px) + var(--s-4)) max(calc(140px + var(--page-bar-b, 0px)), calc(var(--toast-h, 0px) + var(--s-4) - var(--scroll-pad-b, 0px))); }
:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; box-shadow: var(--glow-soft); border-radius: var(--r-1); }
/* clip-path clips outline AND box-shadow, so anything painted outside the polygon
   is discarded: faceted/hex controls take the ring on the inside instead. */
.btn:focus-visible, .chip:focus-visible, .slab:focus-visible, .tab:focus-visible,
.dice-fab:focus-visible, .sound-btn:focus-visible, .avatar:focus-visible,
.shield:focus-visible, .cr:focus-visible {
  outline-offset: -4px; box-shadow: var(--glow-inset); border-radius: 0;
}
/* The same goes for the faceted controls that pages cut with their own
   clip-path: the sheet's «بخش به بخش», the rules reference's previous/next
   cards, the not-found page's ways out. Their ring was drawn outside and the
   clip took every pixel of it, so focus vanished there. Each has a frame
   (--bd), and the frame lights too: the clip takes the ring's cut corners,
   and the lit frame still closes them. */
.pg-fate .sheet-lang__custom > summary:focus-visible,
.pg-well .well-pn__card:focus-visible, .pg-404 .nf-links a:focus-visible {
  --bd: var(--accent); outline-offset: -4px; box-shadow: var(--glow-inset); border-radius: 0;
}
/* A segment (the sheet's language, any .seg) is 36–40px tall with a 4px cut,
   so its ring sits 3px in. It keeps the segment's own shadow, which is the
   chosen one's tint: the choice and the focus stay two different marks, and a
   focused segment that is not chosen shows its ring alone. The outline is
   named here, not inherited: a roving group parks its other segments at
   tabindex -1, and the rule below that blanks a heading's focus would take
   their ring too. */
.seg > button:focus-visible { outline: 2px solid var(--accent); outline-offset: -3px; border-radius: 0; }
/* A ring is only a ring against the surface it is drawn on. --accent IS the emerald
   fill of these controls (1.0:1 on it — the ring was invisible on exactly the
   buttons that matter most), so they take the ink they already print their label
   in: near-black on the dark theme's emerald, near-white on Frost's jade. */
.btn--primary:focus-visible, .cr:focus-visible, .vh-badge__rune:focus-visible {
  outline-color: var(--accent-fg); box-shadow: inset 0 0 0 1px rgba(255,255,255,.35);
}
/* …but a primary that waits («گام بعد» before the step is whole, «فرستادن
   گزارش» before a reason) has no fill: it is the dashed empty socket
   (components.css), and that ink sank into its grey at 2:1. It stays
   focusable so a screen reader can say why it waits, so it takes an ordinary
   control's ring, over the socket's own `box-shadow: none`. */
:root .btn.btn--primary:is([disabled], [aria-disabled="true"]):focus-visible {
  outline-color: var(--accent); box-shadow: var(--glow-inset);
}
/* Pages also fade a waiting button (opacity .5–.65, a desaturate: fate.css,
   misc.css, quests.css), and a fade takes the ring with it, back to 2:1. The
   dashed socket already says "waiting"; while it holds focus it is drawn
   whole, so the ring and the label it describes can be read. The extra
   [class] lifts this over those page rules, which run to four selectors. */
:root .btn[class]:is([disabled], [aria-disabled="true"]):focus-visible { opacity: 1; filter: none; }
/* the hex is the span, so the ring goes inside it — an outline around the button
   would sit on the nav bar, beside the shape it is meant to mark */
.bnav__d20:focus-visible { outline: none; box-shadow: none; }
.bnav__d20:focus-visible span { box-shadow: inset 0 0 0 3px var(--accent-fg); }
/* Route changes move focus to the page heading so screen readers announce it.
   That target is not Tab-reachable, so a painted ring would tell a sighted
   keyboard user nothing while covering the headline. */
[tabindex="-1"]:focus, [tabindex="-1"]:focus-visible, main:focus, main:focus-visible {
  outline: none; box-shadow: none;
}
::selection { background: rgba(47,211,154,.35); color: var(--fg-strong); }
[hidden] { display: none !important; }
.sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: 0; overflow: hidden; clip: rect(0,0,0,0); white-space: nowrap; border: 0; }
.skip-link { position: fixed; top: -100px; inset-inline-start: var(--s-4); z-index: 999; background: var(--accent); color: var(--accent-fg); padding: var(--s-2) var(--s-4); font-weight: 700; }
.skip-link:focus { top: var(--s-4); text-decoration: none; }

/* layout — every grid and flex child may shrink below its content's width;
   the default (min-width: auto) let one long word stretch a whole column */
.wrap { width: min(100% - 2 * var(--gutter), var(--max)); margin-inline: auto; }
.wrap--narrow { width: min(100% - 2 * var(--gutter), 860px); margin-inline: auto; }
.section { padding-block: var(--s-9); position: relative; }
.section--tight { padding-block: var(--s-7); }
.grid { display: grid; gap: var(--s-5); }
.grid > *, .row > *, .stack > * { min-width: 0; }
.grid-2 { grid-template-columns: repeat(2, minmax(0,1fr)); }
.grid-3 { grid-template-columns: repeat(3, minmax(0,1fr)); }
.grid-4 { grid-template-columns: repeat(4, minmax(0,1fr)); }
.row { display: flex; gap: var(--s-3); align-items: center; flex-wrap: wrap; }
.between { justify-content: space-between; }
.stack { display: flex; flex-direction: column; gap: var(--s-4); }
.center { text-align: center; }
.muted { color: var(--fg-muted); }
.faint { color: var(--fg-faint); }
.strong { color: var(--fg-strong); }
.accent { color: var(--accent-text); }
.num { font-variant-numeric: tabular-nums; }
/* A figure is never split: not a sum (۷٬۳۵۰٬۰۰۰), not a sign from its digits
   (+۲, HX.sign), not a range (۴۵/۱۸۰). It may move to the next line whole, at a
   space beside it, but whatever its row says about breaking words (a card
   cell, a user-text block that allows `anywhere`) does not reach inside it. A
   figure that stands alone does not wrap at all. */
:is(.num, .c-amount) { overflow-wrap: normal !important; word-break: normal !important; }
:is(bdi.num, .c-amount, b.num) { white-space: nowrap; }
.nowrap { white-space: nowrap; }
/* A Latin run inside Persian: isolated, in the body face (Vazirmatn's Latin is
   drawn to sit with its Persian). Cinzel is a decorative inscription face —
   lowercase as small caps, old-style digits — so it is opt-in, for the few
   English display names, and never for code, numbers or running words. */
.latin { direction: ltr; unicode-bidi: isolate; }
.latin-display { direction: ltr; unicode-bidi: isolate; font-family: var(--font-latin); }
.mono { font-family: var(--font-mono); font-size: .8125rem; letter-spacing: 0; direction: ltr; unicode-bidi: isolate; }
.rune { font-family: var(--font-rune); letter-spacing: .3em; color: var(--rune-lit); user-select: none; font-size: var(--rune-md); }
@media (max-width: 56.25em) { .grid-3, .grid-4 { grid-template-columns: repeat(2, minmax(0,1fr)); } }
@media (max-width: 37.5em) { .grid-2, .grid-3, .grid-4 { grid-template-columns: 1fr; } }

/* section heading with rune ornament: ᚼ heading ───────── more
   The rune and the rule centre on the H2, not on the H2 + kicker block: the
   kicker drops to its own row under the title. Works with and without the
   wrapper div the pages put around the H2 and the kicker. */
.sec-head {
  display: grid; grid-template-columns: auto auto minmax(40px, 1fr) auto;
  grid-template-areas: "rune title line more" ". kicker kicker kicker";
  align-items: center; column-gap: var(--s-4); row-gap: var(--s-1); margin-bottom: var(--s-6);
}
.sec-head > div { display: contents; }
.sec-head .rune { grid-area: rune; font-size: var(--rune-md); }
.sec-head h2 { grid-area: title; margin: 0; min-width: 0; }
.sec-head .kicker { grid-area: kicker; max-width: 60ch; }
.sec-head > .sec-more, .sec-head > a:last-child { grid-area: more; }
.sec-head::after { content: ""; grid-area: line; height: 1px; background: linear-gradient(to left, var(--line-glow), transparent 70%); }
[dir="ltr"] .sec-head::after { background: linear-gradient(to right, var(--line-glow), transparent 70%); }
@media (max-width: 37.5em) {
  .sec-head { grid-template-columns: auto minmax(0, 1fr) auto; grid-template-areas: "rune title more" ". kicker kicker"; }
  .sec-head::after { display: none; }
}

/* reading text: one full line between paragraphs, on the leading's own grid */
.prose { max-width: var(--measure); }
.prose p { margin-bottom: calc(var(--lh-body) * 1em); }
.prose h2 { font-size: clamp(var(--fs-xl), 1rem + 1.6vw, var(--fs-2xl)); margin-top: 2em; margin-bottom: .5em; }
.prose h3 { font-size: var(--fs-lg); margin-top: 1.6em; }
.prose blockquote { margin: 1.4em 0; padding: var(--s-4) var(--s-5); border-inline-start: 2px solid var(--accent); background: var(--bg-elev); color: var(--fg-strong); }
.prose ul li { margin-bottom: .35em; }
.prose li a { text-decoration: underline; text-decoration-thickness: 1px; text-underline-offset: .25em; text-decoration-color: color-mix(in srgb, currentColor 55%, transparent); }

/* carved heading (text looks cut into stone) */
.carved { color: var(--fg-strong); text-shadow: 0 1px 0 rgba(255,255,255,.06), 0 -1px 0 rgba(0,0,0,.8); }
.lit { color: var(--emerald); text-shadow: 0 0 12px rgba(47,211,154,.6), 0 0 32px rgba(47,211,154,.3); }
/* Frost: ink, not a mint halo */
:root[data-theme="light"] .lit { color: var(--jade); text-shadow: none; }

/* reveal — content starts visible; JS adds .will-reveal only when motion is allowed.
   Opacity only and short: a list that rises 18px per card made the reader read
   the lowest row through a veil while it caught up with the scroll. */
.will-reveal { opacity: 0; }
.revealed { opacity: 1; transition: opacity var(--t-ui) var(--ease-out); }
.revealed > .stagger { transition-delay: calc(min(var(--i, 0), 6) * 40ms); }
/* cards appended by "load more": they arrive, the page does not jump */
.is-entering { animation: hx-enter var(--t-ui) var(--ease-out) both; animation-delay: calc(min(var(--i, 0), 6) * 40ms); }
@keyframes hx-enter { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: none; } }

/* The in-app motion switch (Valhalla) sets data-motion on <html> via core.js.
   Without this block it only gated JS while every CSS animation kept running.
   Delays are zeroed too: a shortened animation that still waited its delay left
   delayed items invisible for exactly the readers who asked for less motion.
   Rule for every keyframe set in this codebase: its 100% is the resting,
   visible state. */
:root[data-motion="off"] *, :root[data-motion="off"] *::before, :root[data-motion="off"] *::after {
  animation-duration: .001ms !important;
  animation-delay: 0s !important;
  animation-iteration-count: 1 !important;
  transition-duration: .001ms !important;
  transition-delay: 0s !important;
  scroll-behavior: auto !important;
}
/* And the same for the reader who asked their system, not this site. The switch
   defaults to on, so without this block a system-wide preference stopped only the
   handful of animations that named it — the breathing d20, the grain and the
   loading ring kept going for exactly the people who asked them not to. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: .001ms !important;
    animation-delay: 0s !important;
    animation-iteration-count: 1 !important;
    transition-duration: .001ms !important;
    transition-delay: 0s !important;
    scroll-behavior: auto !important;
  }
}

/* time-of-day realm variables (set by JS on <html>) */
/* data-realm still names the hour; the sky/aurora colours it used to carry are gone
   with the generated hero scene — the realm is a painted plate per hour now. */
:root { --realm-name: "شب شفق‌زده"; }
.handle { direction: ltr; unicode-bidi: isolate; font-family: inherit; }
/* a member's own words, in whatever script they typed them: laid out and
   truncated from their own start (a Latin name keeps its first word) */
bdi, .user-text { unicode-bidi: isolate; }
/* A holder of one such text (HX.textAttrs, or the net in HX.enhance) takes the
   text's direction, which orders its letters and decides which end an
   ellipsis cuts — and lines up with its row, not by its own direction: a
   Persian name beside its portrait in an English card stays beside it, a
   mostly-Latin name in a Persian card stays on the right with its meta.
   match-parent is "inherit, with start and end read in the parent's
   direction". A line too long for its box is still laid out from its own
   start, so the ellipsis stays at the text's end. Chrome and Safari know the
   value only by its -webkit- name; Firefox only by the standard one. */
[data-iso] { text-align: -webkit-match-parent; text-align: match-parent; }

/* ── Windows high contrast / forced colours ──
   The system repaints every surface; what must survive is the state that was
   carried by tint alone, and the decoration must step aside. */
@media (forced-colors: active) {
  body::after { display: none; }
  :focus-visible { outline: 2px solid Highlight; outline-offset: 2px; }
  .btn:focus-visible, .chip:focus-visible, .tab:focus-visible { outline-offset: -4px; }
  /* a chosen segment is outlined in Highlight here (fate.css), the one mark
     the system leaves it; focus is a heavier ring of its own, further in, so
     the choice and the focus never print as the same line */
  :root .seg > button:focus-visible { outline: 3px solid CanvasText; outline-offset: -4px; }
  .rune, .sec-head::after { forced-color-adjust: auto; }
}

/* What the home page says as plain HTML (web/routes.ts CRAWL_INTRO), for a
   reader that runs no script and for the moment before the script draws over
   it: quiet, centred, in the site's own type. */
.seo-intro { max-width: 40rem; margin: 18vh auto 0; padding: 0 var(--s-4); text-align: center; color: var(--fg-muted); }
.seo-intro h1 { margin: 0 0 var(--s-3); color: var(--fg-strong); font-size: clamp(1.5rem, 4vw, 2.25rem); line-height: 1.4; }
.seo-intro a { color: inherit; }
