/* ==========================================================================
   tystnad.css — playtystnad.com, the one stylesheet
   Layers: tokens → fonts → base → components → patterns → responsive
   Legibility laws (SITE ARCHITECTURE.md) are encoded as component rules;
   peak luminance is bone, red never colors a value's glyphs.
   ========================================================================== */

/* ============ TOKENS ============ */

:root {
  --ink: #0c0a0b;          /* background, near-black */
  --panel: #171416;        /* raised surfaces */
  --panel-hi: #201c1e;     /* pressed / active surfaces */
  --line: #2a2528;         /* hairlines */
  --bone: #cfc9c0;         /* primary text */
  --ash: #7d766f;          /* secondary text, chrome only */
  --blood: #a11e24;        /* accent */
  --blood-bright: #d92b32; /* small accent text, active states */
  --ember-light: #d97a24;
  --ember-strong: #b96a33;
  --health-green: #6b9e57; /* party-HUD health meter only; mossy, fits the fungal dark */
  --font-display: "Cormorant Garamond", Georgia, serif;
  --font-body: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;

  /* LINING FIGURES, GLOBALLY (ruled 2026-08-03).
     Cormorant's default numerals are OLD-STYLE: 0 renders as a small o-shape
     and 1 as an I-shape. In prose that is handsome; on anything a GM has to
     READ AS A NUMBER it is ambiguous, and the Owlbear dice pool was showing
     "o" for an empty die count and "I" for one d20.

     This was invisible until 2026-07-21, because Cormorant had never actually
     loaded (the three woff2 files were GitHub 404 pages) and everything fell
     back to Georgia. Georgia ALSO has old-style figures and carries no `lnum`
     feature to override them -- so the day the font started working, every
     numeral in the product quietly changed shape.

     Set on :root because the property inherits: it reaches every element that
     resolves to --font-display without enumerating them. Harmless to the body
     sans, whose figures are already lining. `tabular-nums` is deliberately NOT
     set here; it would fix digit widths in running prose too. */
  font-variant-numeric: lining-nums;
  --chamfer-8: polygon(8px 0%, calc(100% - 8px) 0%, 100% 8px, 100% calc(100% - 8px), calc(100% - 8px) 100%, 8px 100%, 0% calc(100% - 8px), 0% 8px);
  --chamfer-7: polygon(7px 0%, calc(100% - 7px) 0%, 100% 7px, 100% calc(100% - 7px), calc(100% - 7px) 100%, 7px 100%, 0% calc(100% - 7px), 0% 7px);
  --chamfer-6: polygon(6px 0%, calc(100% - 6px) 0%, 100% 6px, 100% calc(100% - 6px), calc(100% - 6px) 100%, 6px 100%, 0% calc(100% - 6px), 0% 6px);
  --rail-w: 125px;
  --logo-w: 200px;
  /* Logo render height from the asset's 879x564 aspect. */
  --logo-h: calc(var(--logo-w) * 564 / 879);
  --rail-inset: 16px;
  /* Nothing sits higher than the logo's bottom edge (ruled 2026-07-10):
     the band above is reserved for future offers, news, expansions. */
  --content-top: calc(var(--rail-inset) + var(--logo-h) + 1rem);
}

/* ============ FONTS ============ */

/* Cormorant Garamond, self-hosted, weights 500/600/700 (Visual Identity law).
 *
 * ONE VARIABLE FILE, three declarations. Fixed 2026-07-21: the three previous
 * files (Medium/SemiBold/Bold.woff2) were GitHub 404 HTML pages saved with a
 * font extension -- ~311KB of "<!DOCTYPE html>" each -- so Cormorant had NEVER
 * loaded on this site or in the companion app, and every display heading was
 * silently rendering in the Georgia fallback. Check magic bytes on any binary
 * asset: a woff2 begins "wOF2" (77 4f 46 32).
 *
 * Google no longer ships static instances for this family; it is a variable
 * font on the weight axis, one file per unicode subset. This is the `latin`
 * subset (U+0000-00FF plus punctuation), which covers English and Swedish.
 * Each @font-face pins a single weight, so the browser instantiates the axis
 * at 500/600/700 exactly as before. One 37KB download instead of three.
 *
 * SIL Open Font License, so self-hosting is licensed.
 */
@font-face {
  font-family: "Cormorant Garamond";
  src: url("../fonts/CormorantGaramond-Variable.woff2") format("woff2");
  font-weight: 500;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: "Cormorant Garamond";
  src: url("../fonts/CormorantGaramond-Variable.woff2") format("woff2");
  font-weight: 600;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: "Cormorant Garamond";
  src: url("../fonts/CormorantGaramond-Variable.woff2") format("woff2");
  font-weight: 700;
  font-style: normal;
  font-display: swap;
}

/* ============ BASE ============ */

* { margin: 0; padding: 0; box-sizing: border-box; }

/* Texture under a heavy ink scrim (ruled 2026-07-10, law 5 reading A):
   the rock reads as a whisper in the gaps between panels; text keeps
   effectively full contrast. On the root element so it paints the whole
   canvas even though the page content is a fixed scroll region. */
html {
  background-color: var(--ink);
  background-image:
    linear-gradient(rgba(12, 10, 11, 0.8), rgba(12, 10, 11, 0.8)),
    url("../TYSTNAD_Background_1920.jpg");
  background-size: cover;
  background-position: center;
  background-repeat: no-repeat;
  background-attachment: fixed;
  -webkit-text-size-adjust: 100%;
}

body {
  color: var(--bone);
  font-family: var(--font-body);
  font-size: 1rem;
  min-height: 100vh;
}

a { color: inherit; text-decoration: none; }

button {
  font: inherit;
  color: inherit;
  background: none;
  border: none;
  cursor: pointer;
}

input {
  font: inherit;
  color: var(--bone);
  background: none;
  border: none;
}

input:focus { outline: none; }

::placeholder { color: var(--ash); opacity: 1; }

/* Focus signals by blood, never by luminance. */
:focus-visible { outline: 1px solid var(--blood-bright); outline-offset: 2px; }

/* Cross-document view transitions: the logo morphs between pages
   (progressive; browsers without support just navigate). */
@view-transition { navigation: auto; }

/* ============ FX LAYERS (decorative, JS-created, never interactive) ==== */

/* Parallax copy of the texture; sits above the body's static texture. */
.fx-bg {
  position: fixed;
  inset: -24px;
  z-index: -4;
  background-color: var(--ink);
  background-image:
    linear-gradient(rgba(12, 10, 11, 0.8), rgba(12, 10, 11, 0.8)),
    url("../TYSTNAD_Background_1920.jpg");
  background-size: cover;
  background-position: center;
  background-repeat: no-repeat;
  pointer-events: none;
  will-change: transform;
}

.fx-ghost {
  position: fixed;
  right: -3vw;
  bottom: -6vh;
  z-index: -3;
  font-family: var(--font-display);
  font-size: 17vw;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--bone);
  opacity: 0.035;
  pointer-events: none;
  user-select: none;
  white-space: nowrap;
  will-change: transform;
}

.fx-embers {
  position: fixed;
  inset: 0;
  z-index: -2;
  pointer-events: none;
}

.fx-spot {
  position: fixed;
  top: 0;
  left: 0;
  width: 720px;
  height: 720px;
  z-index: -1;
  border-radius: 50%;
  background: radial-gradient(circle,
    rgba(217, 122, 36, 0.05) 0%,
    rgba(217, 43, 50, 0.025) 35%,
    transparent 65%);
  pointer-events: none;
  will-change: transform;
}

/* Entrance cascade: elements arrive, softly and once. */
.fx-in { opacity: 0; transform: translateY(12px); }

.fx-go {
  opacity: 1;
  transform: none;
  transition: opacity 0.6s ease, transform 0.6s cubic-bezier(0.2, 0.7, 0.3, 1);
}

/* Band ticker removed 2026-07-15 — the reserved band is now the party HUD,
   which takes the full width. */

@media (prefers-reduced-motion: reduce) {
  .fx-in { opacity: 1; transform: none; }
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
}

/* ============ COMPONENTS ============ */

/* --- Chamfered panel (45deg / 8px, family signature) --- */

/* Glass-dark: the texture breathes through raised surfaces, a 1px
   bone inner highlight implies a light source above. */
/* CHAMFER BORDER LAW (2026-07-10): never pair `border` with `clip-path`.
   The clip cuts the corner triangles off AFTER the border paints, so the
   border vanishes along the diagonal edge and stops dead at the cuts.
   Technique instead: the element paints the border COLOR as background
   under the outer chamfer; ::before paints the surface at inset 1px under
   the next chamfer down (8->7, 7->6). clip-path creates a stacking
   context, so ::before at z-index -1 sits above the ring, below content.
   Hover "border" changes recolor the element background. The smoke suite
   greps for violations. */

.panel {
  position: relative;
  background: var(--line);
  clip-path: var(--chamfer-8);
}

.panel::before {
  content: "";
  position: absolute;
  inset: 1px;
  z-index: -1;
  background: rgba(23, 20, 22, 0.78);
  backdrop-filter: blur(9px);
  -webkit-backdrop-filter: blur(9px);
  box-shadow: inset 0 1px 0 rgba(207, 201, 192, 0.05);
  clip-path: var(--chamfer-7);
}

/* Chamfer with a full 1px blood border: blood wrapper + inner surface,
   the app's nav-pill technique. */
.frame {
  background: var(--blood);
  clip-path: var(--chamfer-8);
  padding: 1px;
}

.frame > .frame-inner {
  background: var(--panel);
  clip-path: var(--chamfer-7);
}

/* --- Section label: blood-bright small caps (law 1: accent text below
       1.2rem is always blood-bright) --- */

.section-label {
  font-family: var(--font-display);
  font-size: 1.5rem;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--blood-bright);
}

.micro-label {
  font-size: 0.75rem;
  font-weight: 600;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--blood-bright);
}

/* --- Stat chip: the number is the headline, in bone (law 1) --- */

.stat-chip {
  position: relative;
  background: var(--line);
  clip-path: var(--chamfer-8);
  padding: 0.5rem 0.8rem 0.45rem;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.2rem;
  min-width: 4.5rem;
  text-align: center;
}

.stat-chip::before {
  content: "";
  position: absolute;
  inset: 1px;
  z-index: -1;
  background: var(--panel-hi);
  clip-path: var(--chamfer-7);
}

.stat-chip .stat-value {
  font-family: var(--font-display);
  font-size: 1.5rem;
  font-weight: 600;
  line-height: 1;
  color: var(--bone);
}

.stat-chip .stat-note {
  font-size: 0.75rem;
  color: var(--ash);
}

.chip-row {
  display: flex;
  flex-wrap: wrap;
  gap: 0.75rem;
}

/* --- Prose: rules text (law 2) — 16px min, 1.65, 70ch, bone, on a panel --- */

.prose {
  font-size: 1rem;
  line-height: 1.65;
  max-width: 70ch;
  color: var(--bone);
}

.prose + .prose { margin-top: 0.9rem; }

/* Inline decision value inside prose: bone glyphs, 1px blood underline (law 1). */
.val {
  color: var(--bone);
  border-bottom: 1px solid var(--blood);
  padding-bottom: 1px;
}

/* Cross-reference link (link law, ruled 2026-07-10): bone with a DOTTED
   blood underline. Solid blood underline stays exclusive to inline values.
   Hover signals by blood-bright decoration, never by luminance (law 6). */
.term-link {
  color: var(--bone);
  text-decoration: underline dotted var(--blood);
  text-decoration-thickness: 1px;
  text-underline-offset: 3px;
}

.term-link:hover,
.term-link:focus-visible {
  text-decoration-color: var(--blood-bright);
  outline: none;
}

/* Ordered rules procedure list: prose rules apply (law 2). */
.prose-list {
  font-size: 1rem;
  line-height: 1.65;
  max-width: 70ch;
  color: var(--bone);
  margin: 0;
  padding-left: 1.4rem;
}

.prose-list li + li { margin-top: 0.45rem; }

.prose + .prose-list,
.prose-list + .prose { margin-top: 0.9rem; }

/* --- Data rows: label + value tables (ash is chrome, bone carries — law 3) --- */

.data-table { width: 100%; border-collapse: collapse; max-width: 70ch; }

.data-table thead th {
  font-size: 0.75rem;
  font-weight: 600;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--ash);
  text-align: left;
  padding: 0.45rem 1.25rem 0.45rem 0;
  border-bottom: 1px solid var(--line);
}

/* Row headers read like data labels, not column headers. */
.data-table tbody th {
  font-weight: 400;
  text-align: left;
  color: var(--bone);
  padding: 0.4rem 1.25rem 0.4rem 0;
  border-bottom: 1px solid var(--line);
  line-height: 1.45;
}

.data-table td {
  color: var(--bone);
  padding: 0.4rem 1.25rem 0.4rem 0;
  border-bottom: 1px solid var(--line);
  line-height: 1.45;
}

.data-table td.num {
  font-family: var(--font-display);
  font-size: 1.35rem;
  font-weight: 600;
  line-height: 1;
  white-space: nowrap;
}

/* --- Search bar --- */

.search-frame { width: 100%; }

.search-frame .frame-inner {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  padding: 0 1rem;
}

.search-frame input {
  flex: 1;
  min-width: 0;
  height: 3rem;
  font-size: 1rem;
}

.search-frame svg { flex: none; color: var(--ash); }

/* Command-palette chrome: keyboard hint chip, one-breath focus. */
.kbd-hint {
  flex: none;
  font-size: 0.75rem;
  font-weight: 600;
  letter-spacing: 0.08em;
  color: var(--ash);
  border: 1px solid var(--line);
  border-radius: 5px;
  padding: 0.1rem 0.45rem;
}

.search-frame:focus-within {
  filter: drop-shadow(0 0 10px rgba(217, 43, 50, 0.28));
  animation: breath 0.45s ease;
}

@keyframes breath {
  50% { transform: scale(1.012); }
}

.search-hint {
  font-size: 0.75rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--ash);
  margin-top: 0.6rem;
  text-align: center;
}

/* ============ RAIL (Pattern 1 + 2, persistent) ============ */

/* The slot is a column: logo in the top left corner, rail below it.
   The rail keeps its narrow width; the logo takes the full gutter. */
.rail-slot {
  position: fixed;
  top: var(--rail-inset);
  left: var(--rail-inset);
  bottom: var(--rail-inset);
  width: var(--logo-w);
  z-index: 10;
  display: flex;
  flex-direction: column;
  /* Rail centers on the logo's vertical axis. */
  align-items: center;
  /* 1rem, and this is a deliberate reversal of the same day's change.

     Two alignment targets exist here and they are MUTUALLY EXCLUSIVE, because
     the rail's first label lives inside a padded, bordered panel and page
     content does not:

       (a) the rail PANEL's top edge aligns with the first content element
       (b) the first rail LABEL aligns with the first line of content

     A gap of 0 bought (b) and cost (a) -- and cost more than alignment: with no
     gap the logo's dagger descender touched the panel border.

     Ruled by Tomas 2026-08-02, looking at it: (a) wins. The reference he named
     is the Bestiary search field, which is `.list-pane` at `top: 0` inside
     `.page`, so its top IS --content-top. The rail panel now sits on that same
     line, and the logo gets its clearance back. */
  gap: 1rem;
}

/* The rail container is rounded, ruled 2026-07-10; the chamfer remains
   law on every other surface. */
.rail {
  width: var(--rail-w);
  flex: 1;
  min-height: 0;
  background: rgba(23, 20, 22, 0.72);
  backdrop-filter: blur(9px);
  -webkit-backdrop-filter: blur(9px);
  border: 1px solid var(--blood);
  border-radius: 14px;
  box-shadow: inset 0 1px 0 rgba(207, 201, 192, 0.05);
  display: flex;
  flex-direction: column;
  /* Back to 1.25rem with the gap restored above. The 15px existed only to make
     the first LABEL land on the content line, which is the target that lost;
     with the panel edge aligned instead there is no reason to hold an odd
     number, so the panel keeps its designed internal rhythm. */
  padding: 1.25rem 0 1rem;
  /* Scroll the rail's own items when they exceed a short viewport (the fixed
     slot can't grow, so without this the lower items — Table Link, Roll,
     Sign out — are clipped and unreachable). margin-top:auto still pins the
     foot controls when everything fits. */
  overflow-y: auto;
  overscroll-behavior: contain;
}

/* Logo mark in the top left corner: bone paint through a CSS mask of the
   white-on-transparent PNG, so no white pixel ever paints (law 4). */
.rail-logo {
  display: block;
  width: var(--logo-w);
  flex: none;
}

.rail-logo-mark {
  display: block;
  width: 100%;
  aspect-ratio: 879 / 564;
  view-transition-name: tystnad-logo;
  background-color: var(--bone);
  -webkit-mask-image: url("../TYSTNAD_Logo_White_Transparent.png");
  mask-image: url("../TYSTNAD_Logo_White_Transparent.png");
  -webkit-mask-size: contain;
  mask-size: contain;
  -webkit-mask-repeat: no-repeat;
  mask-repeat: no-repeat;
  -webkit-mask-position: center;
  mask-position: center;
}

.rail-nav {
  display: flex;
  flex-direction: column;
  gap: 0.9rem;
  padding: 0 0.4rem;
}

/* Icon above small-caps label. No boxes: hover signals by icon motion,
   active signals by blood (law 6 as amended 2026-07-10). */
.rail-item {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.4rem;
  padding: 0.5rem 0.25rem;
  font-size: 0.75rem;
  font-weight: 600;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--bone);
}

.rail-item svg {
  flex: none;
  color: var(--ash);
  transition: transform 120ms ease;
}

.rail-item:hover svg { transform: scale(1.15); }

/* Active: blood icon + blood label, static; below 1.2rem blood renders
   blood-bright (law). */
.rail-item.active { color: var(--blood-bright); }

.rail-item.active svg { color: var(--blood-bright); }

/* Hairline divider below the nav items. */
.rail-divider {
  height: 1px;
  background: var(--line);
  margin: 0.4rem 0.75rem 0;
}

/* d20 quick roller at the rail foot. Bone label + ash icon, matching the
   content nav items (Conflict etc.); the whole rail reads as one cluster. */
.rail-roller {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.4rem;
  padding: 0.5rem 0.5rem;
  font-size: 0.75rem;
  font-weight: 600;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--bone);
}

.rail-roller svg { color: var(--ash); transition: transform 120ms ease; }

.rail-roller:hover { color: var(--bone); }

.rail-roller:hover svg { transform: scale(1.15) rotate(12deg); }

/* My Account: a foot utility just above Sign out, styled to match the rail
   foot cluster (ash, hover to bone). Jumps to the account portal. */
.rail-account {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.4rem;
  padding: 0.5rem 0.5rem;
  margin-top: 0.4rem;
  font-size: 0.7rem;
  font-weight: 600;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--bone);
}
.rail-account svg { color: var(--ash); transition: transform 120ms ease; }
.rail-account:hover { color: var(--bone); }
.rail-account:hover svg { color: var(--bone); transform: scale(1.12); }
.rail-account.active { color: var(--bone); }
.rail-account.active svg { color: var(--bone); }

/* My Account in-pane view (/me/): read-only rows + link-out action buttons. */
.account-row {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 1rem;
  padding: 0.5rem 0;
  border-bottom: 1px solid var(--line);
  font-size: 0.9rem;
}
.account-action {
  display: inline-block;
  margin-top: 0.7rem;
  font-size: 0.75rem;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--bone);
  background: transparent;
  border: 1px solid var(--line);
  border-radius: 4px;
  padding: 0.45rem 0.85rem;
  text-decoration: none;
  transition: border-color 120ms ease, color 120ms ease;
}
.account-action:hover { border-color: var(--blood); color: var(--blood-bright); }
.account-note { margin-top: 0.75rem; font-size: 0.8rem; }

/* Sign out: a muted utility at the very foot, below the roller. Hover
   signals leaving in blood-bright (law 6: recolor, no surface lightening). */
.rail-logout {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.4rem;
  padding: 0.5rem 0.5rem;
  margin-top: 0.4rem;
  font-size: 0.7rem;
  font-weight: 600;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--bone);
}

.rail-logout svg { color: var(--ash); transition: transform 120ms ease; }

.rail-logout:hover,
.rail-logout:hover svg { color: var(--blood-bright); }

.rail-logout:hover svg { transform: scale(1.1); }

.roll-overlay {
  position: fixed;
  inset: 0;
  z-index: 100;
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgba(12, 10, 11, 0.65);
  backdrop-filter: blur(5px);
  -webkit-backdrop-filter: blur(5px);
  cursor: pointer;
}

.roll-card-inner {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.4rem;
  padding: 2rem 3.5rem 1.5rem;
}

.roll-value {
  font-family: var(--font-display);
  font-size: 5rem;
  font-weight: 700;
  line-height: 1;
  color: var(--bone);
  min-width: 2ch;
  text-align: center;
}

.roll-value.crit { color: var(--ember-light); }

.roll-value.fumble { color: var(--blood-bright); }

/* --- Dice roller (rail "Dice Roller"): pool builder + modifier + result --- */
.dice-card { cursor: default; }
.dice-inner {
  gap: 0.85rem;
  padding: 1.5rem 1.6rem 1.2rem;
  min-width: min(21rem, 88vw);
  max-width: 92vw;
}
.dice-buttons {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 0.4rem;
}
.dice-btn {
  font-family: var(--font-display);
  font-size: 1rem;
  font-weight: 600;
  color: var(--bone);
  background: var(--panel-hi);
  border: 1px solid var(--line);
  border-radius: 6px;
  padding: 0.55rem 0.7rem;
  min-width: 3rem;
  cursor: pointer;
  transition: border-color 120ms ease, color 120ms ease;
}
.dice-btn:hover { border-color: var(--blood); color: var(--blood-bright); }
.dice-pool {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 0.35rem;
  min-height: 1.7rem;
  align-items: center;
}
.dice-pool-empty { color: var(--ash); font-size: 0.8rem; }
.dice-chip {
  font-size: 0.85rem;
  font-weight: 600;
  color: var(--bone); /* law 4: peak luminance is bone, no white on this site */
  background: var(--blood);
  border: none;
  border-radius: 999px;
  padding: 0.28rem 0.7rem;
  cursor: pointer;
}
.dice-chip::after { content: " ×"; color: var(--bone); }
/* law 6: hover darkens, never lightens the surface (was blood -> blood-bright,
   a luminance spike). Darken is uniform across rounded and chamfered buttons,
   survives clip-path, and keeps bone text on blood at AA. */
.dice-chip:hover { filter: brightness(0.85); }
.dice-mod {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.55rem;
}
.dice-mod-label {
  font-size: 0.7rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--ash);
}
.dice-mod-btn {
  width: 2rem;
  height: 2rem;
  font-size: 1.1rem;
  line-height: 1;
  color: var(--bone);
  background: var(--panel-hi);
  border: 1px solid var(--line);
  border-radius: 6px;
  cursor: pointer;
}
.dice-mod-btn:hover { border-color: var(--blood); color: var(--blood-bright); }
.dice-mod-val {
  font-family: var(--font-display);
  font-size: 1.2rem;
  color: var(--bone);
  min-width: 2.6ch;
  text-align: center;
}
.dice-actions {
  display: flex;
  gap: 0.5rem;
  width: 100%;
}
.dice-clear-btn,
.dice-roll-btn {
  flex: 1;
  font-size: 0.8rem;
  font-weight: 600;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  padding: 0.6rem;
  border-radius: 4px;
  border: 1px solid var(--line);
  cursor: pointer;
}
.dice-clear-btn { background: transparent; color: var(--ash); }
.dice-clear-btn:hover { color: var(--bone); }
.dice-roll-btn { background: var(--blood); color: var(--bone); border-color: var(--blood); }
.dice-roll-btn:hover { filter: brightness(0.85); } /* law 6: darken, never lighten */
.dice-roll-btn:disabled { opacity: 0.4; cursor: not-allowed; }
.dice-result:empty { display: none; }
.dice-result {
  width: 100%;
  border-top: 1px solid var(--line);
  padding-top: 0.75rem;
  display: flex;
  flex-direction: column;
  gap: 0.35rem;
}
.dice-line { display: flex; align-items: baseline; gap: 0.6rem; }
.dice-line-label {
  font-size: 0.75rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ash);
  min-width: 2.8ch;
}
.dice-faces { display: flex; flex-wrap: wrap; gap: 0.3rem 0.55rem; }
.dice-face { font-family: var(--font-display); font-size: 1.1rem; color: var(--bone); }
.dice-face.crit { color: var(--ember-light); }
.dice-face.fumble { color: var(--blood-bright); }
.dice-total-row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 0.8rem;
  border-top: 1px solid var(--line);
  padding-top: 0.5rem;
  margin-top: 0.15rem;
}
.dice-total-parts {
  font-size: 0.72rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--ash);
}
.dice-total {
  font-family: var(--font-display);
  font-size: 2.6rem;
  font-weight: 700;
  line-height: 1;
  color: var(--bone);
}
.dice-result.rolled { animation: dicePop 220ms ease; }
@keyframes dicePop {
  from { opacity: 0.25; transform: translateY(4px); }
  to { opacity: 1; transform: none; }
}

/* Rail scroll affordance: bottom fade + chevron when items sit below the fold. */
.rail-slot::after {
  content: "▾";
  position: absolute;
  bottom: 0;
  left: 50%;
  transform: translateX(-50%);
  width: var(--rail-w);
  height: 2.2rem;
  display: flex;
  align-items: flex-end;
  justify-content: center;
  padding-bottom: 0.3rem;
  font-size: 0.8rem;
  color: var(--ash);
  background: linear-gradient(to bottom, transparent, rgba(23, 20, 22, 0.96));
  border-radius: 0 0 14px 14px;
  pointer-events: none;
  opacity: 0;
  transition: opacity 160ms ease;
  z-index: 2;
}
.rail-slot.has-more::after { opacity: 1; }

/* --- Quick Ref: the ★ toggle + the tagged-rules page --- */
.detail-head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 1rem;
}
.detail-head-titles { min-width: 0; }
.qr-star {
  flex: none;
  background: none;
  border: none;
  cursor: pointer;
  font-size: 1.5rem;
  line-height: 1;
  color: var(--ash);
  padding: 0.2rem 0.4rem;
  margin-top: 0.15rem;
  transition: color 120ms ease, transform 120ms ease;
}
.qr-star:hover { color: var(--bone); transform: scale(1.12); }
.qr-star.on { color: var(--ember-light); }
.qr-star-inline {
  display: inline;
  font-size: 1rem;
  padding: 0;
  margin: 0;
  vertical-align: baseline;
}

/* Quick Ref page: one centered column of tagged rules (no list/detail split). */
.qr-page .detail-pane { max-width: 46rem; margin: 0 auto; }
.qr-count {
  font-size: 0.72rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ash);
  margin: 0 0 0.75rem;
}
.qr-rule + .qr-rule {
  margin-top: 2rem;
  padding-top: 2rem;
  border-top: 1px solid var(--line);
}
.qr-empty { text-align: center; padding: 2rem 0; }
.qr-empty .detail-title { margin-bottom: 0.75rem; }
.qr-empty-link {
  display: inline-block;
  margin-top: 1.25rem;
  font-size: 0.8rem;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--blood-bright);
}

.roll-close {
  position: relative;
  font-size: 0.75rem;
  font-weight: 600;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--ash);
  background: var(--line);
  clip-path: var(--chamfer-7);
  padding: 0.35rem 1rem;
  margin-top: 0.5rem;
}

.roll-close::before {
  content: "";
  position: absolute;
  inset: 1px;
  z-index: -1;
  background: var(--panel);
  clip-path: var(--chamfer-6);
}

.roll-close:hover { color: var(--bone); background: var(--blood); }

/* --- Table roller (R1): stage dice + named outcome --- */

.roll-card-table .roll-card-inner {
  padding: 2rem 2.5rem 1.5rem;
  max-width: min(88vw, 30rem);
}

.roll-stages {
  display: flex;
  gap: 2.25rem;
  justify-content: center;
}

.roll-stage {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.2rem;
}

.roll-stage-value { font-size: 3rem; }

.roll-outcome {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.3rem;
  margin-top: 0.6rem;
  text-align: center;
  min-height: 5.5rem;
}

.roll-outcome-name {
  font-family: var(--font-display);
  font-size: 1.5rem;
  font-weight: 600;
  color: var(--blood-bright);
  line-height: 1.15;
}

.roll-outcome-text {
  font-size: 0.95rem;
  color: var(--bone);
  line-height: 1.5;
  max-width: 26rem;
}

.roll-actions {
  display: flex;
  gap: 0.75rem;
}

/* In-page roll trigger: the roll-close ring grown to a tap target. */
.roll-table-btn {
  position: relative;
  font-size: 0.8rem;
  font-weight: 600;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--blood-bright);
  background: var(--blood);
  clip-path: var(--chamfer-7);
  padding: 0.6rem 1.4rem;
  margin-top: 0.4rem;
}

.roll-table-btn::before {
  content: "";
  position: absolute;
  inset: 1px;
  z-index: -1;
  background: var(--panel);
  clip-path: var(--chamfer-6);
}

.roll-table-btn:hover { background: var(--blood-bright); color: var(--bone); }

/* Badge pill for new rail items (R1's EXPEDITION wears it first):
   small caps, blood-bright text, 1px blood border, transparent fill.
   0.75rem is the font-size floor, law. */
.rail-badge {
  position: relative;
  font-size: 0.75rem;
  font-weight: 600;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--blood-bright);
  background: var(--blood);
  padding: 0.05rem 0.45rem;
  clip-path: var(--chamfer-7);
}

.rail-badge::before {
  content: "";
  position: absolute;
  inset: 1px;
  z-index: -1;
  background: var(--panel);
  clip-path: var(--chamfer-6);
}

/* ============ PAGE SHELL ============ */

/* The page is a fixed scroll region pinned below the reserved band:
   content scrolling up is clipped at the band line, never above it. */
.page {
  position: fixed;
  top: var(--content-top);
  bottom: 0;
  left: calc(var(--rail-inset) + var(--logo-w));
  right: 0;
  overflow-y: auto;
  padding: 0 var(--rail-inset) var(--rail-inset);
}

/* ============ PATTERN 1 — THE LAUNCHER ============ */

/* No own top padding: the launcher starts at the reserved-band line
   (content-top), aligned with the logo's lower edge like every page. */
.launcher {
  max-width: 46rem;
  margin: 0 auto;
  display: flex;
  flex-direction: column;
  align-items: center;
}

.hero-line {
  font-family: var(--font-display);
  font-size: clamp(2.6rem, 5.5vw, 4.4rem);
  font-weight: 600;
  color: var(--bone);
  text-align: center;
  line-height: 1.08;
}

.hero-sub {
  font-size: 0.9rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--ash);
  text-align: center;
  margin: 0.9rem 0 2.5rem;
}

.launcher .search-frame { max-width: 34rem; }

.search-results {
  width: 100%;
  max-width: 34rem;
  margin-top: 0.75rem;
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
}

.result-row {
  position: relative;
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 1rem;
  padding: 0.6rem 1rem;
  background: var(--line);
  clip-path: var(--chamfer-7);
  color: var(--bone);
}

.result-row::before {
  content: "";
  position: absolute;
  inset: 1px;
  z-index: -1;
  background: var(--panel);
  clip-path: var(--chamfer-6);
}

.result-row:hover, .result-row:focus-visible { background: var(--blood); outline: none; }

.result-row.kb-active { background: var(--blood); }
.result-row.kb-active::before { background: var(--panel-hi); }

.result-row .result-module {
  font-size: 0.75rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--ash);
  flex: none;
}

/* Bento: tiles of unequal size, live data inside. */
.bricks {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 1rem;
  width: 100%;
  margin-top: 2.5rem;
}

.brick {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  padding: 1.4rem 1.5rem;
  background: var(--line);
  clip-path: var(--chamfer-8);
  transition: background-color 160ms ease, filter 200ms ease;
}

.brick::before {
  content: "";
  position: absolute;
  inset: 1px;
  z-index: -1;
  background: rgba(23, 20, 22, 0.62);
  backdrop-filter: blur(9px);
  -webkit-backdrop-filter: blur(9px);
  box-shadow: inset 0 1px 0 rgba(207, 201, 192, 0.05);
  clip-path: var(--chamfer-7);
}

.brick-wide { grid-column: span 2; }

/* Hover: blood glow (drop-shadow survives the chamfer clip) and a
   single sheen sweep across the tile. */
.brick:hover {
  background: var(--blood);
  filter: drop-shadow(0 0 14px rgba(217, 43, 50, 0.28));
}

.brick::after {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(115deg,
    transparent 42%, rgba(217, 43, 50, 0.09) 50%, transparent 58%);
  transform: translateX(-130%);
  pointer-events: none;
}

.brick:hover::after { animation: sheen 0.9s ease forwards; }

@keyframes sheen {
  to { transform: translateX(130%); }
}

.brick svg { color: var(--ash); }

/* Live data strip inside a tile. */
.brick-live {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 0.6rem;
  margin-top: auto;
  padding-top: 0.9rem;
}

.mini-chip {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.15rem;
  padding: 0.45rem 0.8rem 0.35rem;
  background: var(--line);
  clip-path: var(--chamfer-7);
}

.mini-chip::before {
  content: "";
  position: absolute;
  inset: 1px;
  z-index: -1;
  background: rgba(32, 28, 30, 0.85);
  clip-path: var(--chamfer-6);
}

.mini-chip-label {
  font-size: 0.75rem;
  font-weight: 600;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--blood-bright);
}

.mini-chip-value {
  font-family: var(--font-display);
  font-size: 1.6rem;
  font-weight: 600;
  line-height: 1;
  color: var(--bone);
}

.beast-spotlight {
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
  padding: 0.45rem 0.8rem 0.35rem;
}

.beast-spotlight-label {
  font-size: 0.75rem;
  font-weight: 600;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--ash);
}

.beast-spotlight-name {
  font-family: var(--font-display);
  font-size: 1.35rem;
  font-weight: 600;
  line-height: 1.1;
  color: var(--bone);
  border-bottom: 1px solid var(--blood);
}

.recent-list {
  display: flex;
  flex-direction: column;
  gap: 0.2rem;
  margin-top: 0.4rem;
}

.recent-row {
  position: relative;
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 1rem;
  padding: 0.4rem 0.5rem;
  color: var(--bone);
  background: transparent;
  clip-path: var(--chamfer-7);
}

.recent-row::before {
  content: "";
  position: absolute;
  inset: 1px;
  z-index: -1;
  background: transparent;
  clip-path: var(--chamfer-6);
}

.recent-row:hover { background: var(--blood); }
.recent-row:hover::before { background: var(--panel); }

.brick .brick-name {
  font-family: var(--font-display);
  font-size: 1.5rem;
  font-weight: 600;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--blood-bright);
}

.brick .brick-desc { font-size: 0.85rem; color: var(--ash); line-height: 1.5; }

/* ============ PATTERN 2 — THE COMPENDIUM ============ */

.compendium {
  display: grid;
  grid-template-columns: 280px minmax(0, 1fr);
  gap: 1rem;
  align-items: start;
}

.list-pane {
  position: sticky;
  /* Sticky within the page scroll region, which starts at the band. */
  top: 0;
  max-height: calc(100vh - var(--content-top) - var(--rail-inset));
  display: flex;
  flex-direction: column;
}

.list-pane .search-frame { margin-bottom: 0.75rem; flex: none; }

/* The panel never scrolls itself (its ::before surface would scroll
   away with the content); .entry-scroll inside it does. */
.entry-list {
  display: flex;
  flex-direction: column;
  min-height: 0;
  overflow: hidden;
}

.entry-scroll {
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
  padding: 0.75rem;
}

.entry-group-label {
  font-size: 0.75rem;
  font-weight: 600;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--ash);
  padding: 0.9rem 0.75rem 0.3rem;
}

/* --- View toggle (rules module): By Chapter / A-Z over one flat list.
   Text buttons; active signals by blood-bright, hover by dotted blood,
   never by luminance (law 6). --- */
.view-toggle {
  display: flex;
  gap: 1rem;
  padding: 0.15rem 0.25rem 0;
}

.view-toggle-btn {
  background: none;
  border: none;
  cursor: pointer;
  font-size: 0.75rem;
  font-weight: 600;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--ash);
  /* Tap-target floor (Stability day sweep 2026-07-12): the toggle is a
     real control, not inline prose; give it a finger-sized hit area. */
  padding: 0.35rem 0.4rem;
  min-height: 2.5rem;
  display: inline-flex;
  align-items: center;
}

.view-toggle-btn:hover {
  text-decoration: underline dotted var(--blood-bright);
  text-underline-offset: 3px;
}

.view-toggle-btn.active { color: var(--blood-bright); }

.entry-row {
  position: relative;
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 0.75rem;
  padding: 0.55rem 0.75rem;
  text-align: left;
  width: 100%;
  color: var(--bone);
  background: transparent;
  clip-path: var(--chamfer-7);
}

.entry-row::before {
  content: "";
  position: absolute;
  inset: 1px;
  z-index: -1;
  background: transparent;
  clip-path: var(--chamfer-6);
}

.entry-row .entry-tag {
  font-size: 0.75rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--ash);
  flex: none;
}

.entry-row:hover { background: var(--blood); }
.entry-row:hover::before { background: var(--panel); }

.entry-row.active { color: var(--blood-bright); }
.entry-row.active::before { background: var(--panel-hi); }
.entry-row.active:hover { background: var(--blood); }
.entry-row.active:hover::before { background: var(--panel-hi); }

.list-empty { padding: 0.75rem; font-size: 0.85rem; color: var(--ash); }

.detail-pane { padding: 2rem 2.25rem 3rem; }

.detail-title {
  font-family: var(--font-display);
  font-size: 2.2rem;
  font-weight: 600;
  line-height: 1.1;
  color: var(--bone);
}

.detail-kicker {
  font-size: 0.8rem;
  font-weight: 600;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--ash);
  margin-bottom: 0.3rem;
}

.detail-section { margin-top: 1.15rem; }

.detail-section > .micro-label { display: block; margin-bottom: 0.65rem; }

.detail-section .chip-row + .prose,
.detail-section .data-table + .prose { margin-top: 0.9rem; }

.detail-hint { font-size: 0.8rem; color: var(--ash); margin-top: 1.5rem; }

/* Visible load-failure line: bone on panel, never a blank pane. */
.load-error {
  position: relative;
  padding: 1rem 1.25rem;
  background: var(--blood);
  clip-path: var(--chamfer-8);
  color: var(--bone);
  max-width: 70ch;
}

.load-error::before {
  content: "";
  position: absolute;
  inset: 1px;
  z-index: -1;
  background: var(--panel);
  clip-path: var(--chamfer-7);
}

/* ============ RESPONSIVE — graceful degradation below 900px ============ */

@media (max-width: 900px) {
  .rail-slot {
    position: static;
    width: auto;
    margin: var(--rail-inset);
    height: auto;
    align-items: stretch;
  }

  .rail {
    width: auto;
    flex: none;
    flex-direction: row;
    align-items: center;
    flex-wrap: wrap;
    gap: 0.5rem;
    padding: 0.6rem 0.9rem;
  }

  .rail-logo { width: 3.5rem; }

  .rail-nav { flex-direction: row; flex-wrap: wrap; padding: 0; gap: 0.5rem; }

  .rail-divider { display: none; }

  .page {
    position: static;
    overflow: visible;
    padding: 0 var(--rail-inset) var(--rail-inset);
    min-height: 100vh;
  }

  .launcher { padding-top: 6vh; }

  .bricks { grid-template-columns: 1fr; }

  .brick-wide { grid-column: auto; }

  .rail-roller { margin: 0 0 0 auto; }

  .fx-ghost, .fx-ticker { display: none; }

  .compendium { grid-template-columns: 1fr; }

  .list-pane { position: static; max-height: 40vh; }

  .detail-pane { padding: 1.25rem 1rem 2rem; }
}

/* ==========================================================================
   PLAYER SLOTS - the reserved band earning its keep. Six Table Link seats
   between the logo and the ticker: three pairs side by side, each pair
   stacked (grid flows down the columns, so a pair is a column). Ash
   placeholders until the Companion apps fill them (Phase 5); ui.js
   renders live data into the same boxes when it arrives.
   ========================================================================== */

/* Band boxes top-align with the logo's VISIBLE upper edge (ruled
   2026-07-10), measured live at +26px from the inset: the logo element
   sits 12px below the inset and the PNG carries ~14px of transparent
   headroom above the cross tip. */
.band-slots {
  position: fixed;
  top: calc(var(--rail-inset) + 26px);
  left: calc(var(--rail-inset) + var(--logo-w) + 2rem);
  /* The party HUD takes all available horizontal space (ticker removed 2026-07-15):
     from just right of the logo column to the right inset. */
  width: calc(100vw - var(--rail-inset) - var(--logo-w) - 2rem - var(--rail-inset));
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  grid-template-rows: repeat(2, auto);
  grid-auto-flow: column;
  gap: 8px 14px;
  z-index: 5;
}

.player-slot {
  position: relative;
  background: var(--line);
  clip-path: var(--chamfer-7);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0.45rem 0.6rem;
  min-width: 0;
}

.player-slot::before {
  content: "";
  position: absolute;
  inset: 1px;
  z-index: -1;
  background: rgba(23, 20, 22, 0.62);
  backdrop-filter: blur(9px);
  -webkit-backdrop-filter: blur(9px);
  clip-path: var(--chamfer-6);
}

/* GM-017: presence. The HUD showed a snapshot of any age as confidently as a
   live one; the dot says whether the numbers beside it can still be believed.
   Colour is never the only carrier: every dot also has a title and an
   aria-label ("Online" / "Offline, last heard 4 min ago"), and the curtain
   spells it out in words. An empty seat has no dot at all, since no player
   means no presence to report. */
.seat-dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  flex: 0 0 auto;
  margin-right: 0.45rem;
  background: transparent;
}
/* Offline is ASH, not blood (ruled by Tomas 2026-07-27, on a flag raised when the
   first cut used red). Red already means 81-99% damage on the health meter an
   inch below, so a healthy-but-offline player would have shown a green bar
   beside a red dot: two reds and two greens meaning different things in one
   glance. Ash says "absent" and leaves the alarm colour to the alarm. */
.seat-dot.online { background: var(--health-green); }
.seat-dot.offline { background: var(--ash); }
.curtain-presence { display: inline-flex; align-items: center; }

.player-slot-text {
  font-size: 0.75rem;
  font-weight: 600;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--ash);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* Party HUD: the boxes are buttons now; a filled seat brightens, the open
   seat glows blood. */
.player-slot { border: none; cursor: pointer; font: inherit; width: 100%; text-align: center; }
.player-slot.filled .player-slot-text,
.player-slot:hover .player-slot-text { color: var(--bone); }
.player-slot.active::before { background: rgba(161, 30, 36, 0.3); }

/* The vitals curtain that drops below the band on a box click. Positioned by
   JS (top/left under the band). */
.party-curtain {
  position: fixed;
  z-index: 20;
  width: min(22rem, 90vw);
  background: rgba(23, 20, 22, 0.95);
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
  border: 1px solid var(--blood);
  border-radius: 12px;
  padding: 1rem 1.15rem 1.15rem;
  box-shadow: 0 12px 40px rgba(0, 0, 0, 0.5);
  animation: curtainDrop 160ms ease;
}
@keyframes curtainDrop { from { opacity: 0; transform: translateY(-8px); } to { opacity: 1; transform: none; } }
.curtain-close {
  position: absolute;
  top: 0.45rem;
  right: 0.6rem;
  background: none;
  border: none;
  color: var(--ash);
  font-size: 1.35rem;
  line-height: 1;
  cursor: pointer;
  padding: 0.1rem 0.35rem;
}
.curtain-close:hover { color: var(--bone); }
.curtain-kicker {
  font-size: 0.7rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--blood-bright);
  margin: 0;
}
.curtain-name {
  font-family: var(--font-display);
  font-size: 1.5rem;
  color: var(--bone);
  margin: 0.1rem 0 0.75rem;
  line-height: 1;
}
.curtain-empty { color: var(--ash); font-size: 0.9rem; margin: 0.25rem 0 0; }
.curtain-vital {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  padding: 0.45rem 0;
  border-top: 1px solid var(--line);
}
.curtain-vital-label {
  flex: none;
  width: 5.5rem;
  font-size: 0.7rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ash);
}
.curtain-vital-value { font-size: 0.9rem; color: var(--bone); }

/* Health meter: fill width = HP remaining, colour = damage band. No numbers. */
.hp-meter {
  flex: 1;
  height: 12px;
  border-radius: 6px;
  background: var(--line);
  overflow: hidden;
  position: relative;
}
.hp-fill { height: 100%; border-radius: 6px; transition: width 220ms ease; }
.hp-meter.green .hp-fill { background: var(--health-green); }
.hp-meter.amber .hp-fill { background: var(--ember-light); }
.hp-meter.red .hp-fill { background: var(--blood-bright); }
.hp-meter.unknown { opacity: 0.4; }
.hp-meter.flatline { background: transparent; border: 1px solid var(--line); }
.hp-flatline {
  position: absolute;
  top: 50%;
  left: 6%;
  right: 6%;
  height: 2px;
  background: var(--ash);
  transform: translateY(-50%);
}

/* The band collapses with the rail on phones; the slots + curtain go with it. */
@media (max-width: 900px) {
  .band-slots,
  .party-curtain { display: none; }
}

/* ==========================================================================
   LOGIN GATE (/login/) - the one page outside the session check.
   Centered chamfered panel on the ink; logo painted bone via mask (law 4);
   hover brightens borders, never surfaces (law 6).
   ========================================================================== */

.login-stage {
  min-height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--rail-inset);
}

.login-frame {
  width: 100%;
  max-width: 24rem;
}

.login-panel {
  padding: 2.25rem 2rem 2rem;
}

.login-logo {
  width: 100%;
  aspect-ratio: 879 / 564;
  margin: 0 auto 1.75rem;
  background-color: var(--bone);
  -webkit-mask-image: url("../TYSTNAD_Logo_White_Transparent.png");
  mask-image: url("../TYSTNAD_Logo_White_Transparent.png");
  -webkit-mask-size: contain;
  mask-size: contain;
  -webkit-mask-repeat: no-repeat;
  mask-repeat: no-repeat;
  -webkit-mask-position: center;
  mask-position: center;
}

.login-label {
  display: block;
  font-size: 0.75rem;
  font-weight: 600;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--ash);
  margin: 0 0 0.4rem;
}

/* Chamfered 1px blood border via the frame technique (a border on a
   clipped element loses its corners; the wrapper does not). */
.login-field {
  background: var(--blood);
  clip-path: var(--chamfer-8);
  padding: 1px;
  margin-bottom: 1.25rem;
}

.login-input {
  display: block;
  width: 100%;
  background: var(--panel);
  clip-path: var(--chamfer-7);
  border: none;
  outline: none;
  color: var(--bone);
  font-family: var(--font-body);
  font-size: 1rem;
  padding: 0.7rem 0.9rem;
}

.login-field:focus-within {
  background: var(--blood-bright);
}

.login-submit-wrap {
  background: var(--blood);
  clip-path: var(--chamfer-8);
  padding: 1px;
  margin-top: 0.35rem;
}

.login-submit-wrap:hover,
.login-submit-wrap:focus-within {
  background: var(--blood-bright);
}

.login-submit {
  display: block;
  width: 100%;
  background: var(--blood);
  clip-path: var(--chamfer-7);
  border: none;
  outline: none;
  cursor: pointer;
  color: var(--bone);
  font-size: 0.9rem;
  font-weight: 600;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  padding: 0.8rem 1rem;
}

.login-error {
  margin: 1rem 0 0;
  font-size: 0.85rem;
  color: var(--blood-bright);
  text-align: center;
}

/* ============ CONTENT ART (bestiary portraits, shared component) ====== */
/* Bone plates (ruled 2026-07-11, replacing the rejected inversion): the
   drawing stays ink on paper; prepare_art.py repaints the paper bone and
   the ink near-black, so a plate never outshines the site's own text
   (law 4). The plate sits in a chamfered blood frame: frame technique
   (blood host, image clipped one step down), never border + clip-path.
   Hover recolors the frame blood-bright (the ring-recolor pattern,
   law 6), never lightens the plate. */

.art-panel {
  margin: 0;
}

/* Lead row below the creature name: prose left, framed plate right.
   Without art the single cell simply fills the row. */
.detail-lead {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  gap: 0 24px;
  align-items: start;
}

@media (max-width: 900px) {
  .detail-lead { grid-template-columns: minmax(0, 1fr); }
  .detail-lead .art-panel { margin-top: 1.1rem; justify-self: center; }
}

/* Chips inside the lead's text column sit under the prose. */
.detail-lead .chip-row { margin-top: 0.9rem; }

/* Paired short sections (Strength/Weakness) share a row on wide screens. */
.detail-duo {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 0 24px;
}

@media (max-width: 900px) {
  .detail-duo { grid-template-columns: minmax(0, 1fr); }
}

/* Table footnotes hang close to their table, not a section away. */
.detail-section .data-table + .detail-hint { margin-top: 0.6rem; }

/* Square frame, ruled 2026-07-11: the plate is square, so the ring and
   mat follow it edge to edge. No chamfer here; the cut corners fought
   the square art (chamfer remains law on panels and chips). */
.art-open {
  display: block;
  margin: 0;
  width: fit-content;
  max-width: 100%;
  background: var(--blood);
  padding: 1px;
  cursor: zoom-in;
}

/* The mat: dark mount between the blood ring and the plate. */
.art-mat {
  display: block;
  background: var(--panel-hi);
  padding: 9px;
}

.art-image {
  display: block;
  max-width: 100%;
  max-height: 240px;
  width: auto;
  height: auto;
}

.art-open:hover,
.art-open:focus-visible {
  background: var(--blood-bright);
}

/* Thumbnail strip: appears only when an entry has several images.
   Chamfered ring per the chamfer border law (background under the outer
   chamfer, ::before paints the surface at inset 1px). */
.art-thumbs {
  display: flex;
  gap: 10px;
  justify-content: center;
  margin-top: 12px;
}

.art-thumb {
  position: relative;
  width: 56px;
  padding: 3px;
  background: var(--line);
  clip-path: var(--chamfer-7);
  cursor: pointer;
}

.art-thumb::before {
  content: "";
  position: absolute;
  inset: 1px;
  z-index: -1;
  background: var(--panel);
  clip-path: var(--chamfer-6);
}

.art-thumb img {
  display: block;
  width: 100%;
  height: auto;
}

.art-thumb:hover,
.art-thumb.active {
  background: var(--blood);
}

/* Full-screen viewer: the roller's dialog contract, sized for art. */
.art-overlay {
  position: fixed;
  inset: 0;
  z-index: 100;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 1rem;
  background: rgba(12, 10, 11, 0.94);
  backdrop-filter: blur(5px);
  -webkit-backdrop-filter: blur(5px);
  cursor: pointer;
}

.art-full {
  display: block;
  max-width: 92vw;
  max-height: 84vh;
}

.art-caption {
  font-family: var(--font-display);
  font-size: 1.4rem;
  font-weight: 600;
  color: var(--bone);
}

.art-close {
  cursor: pointer;
}

/* --- Initiative Calculator (R3) -------------------------------------------
   Warn never block: it sums and rolls, the GM rules. Form controls use the
   frame technique (blood wrapper + inner surface); the chamfer border law
   forbids pairing a CSS border with a clip-path. Law 1: the die and the
   totals are bone at scale. Law 6: hover signals by blood, never by
   lightening a surface. */

.calc-roster {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  margin-bottom: 0.85rem;
}

.calc-member {
  display: grid;
  grid-template-columns: auto 1fr 1fr auto auto;
  align-items: center;
  gap: 0.6rem;
}

.calc-member-label {
  font-size: 0.75rem;
  font-weight: 600;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--ash);
  white-space: nowrap;
}

.calc-member-mod {
  font-family: var(--font-display);
  font-size: 1.5rem;
  font-weight: 600;
  color: var(--bone);
  min-width: 2.2ch;
  text-align: right;
}

/* A joined player's row (2026-07-30): name, provenance, modifier. It carries
   its OWN three-column layout at every width rather than inheriting the manual
   row's, and the doubled class keeps it winning wherever it sits in the file.

   Why, learned by looking rather than reasoning: below 640px `.calc-member`
   switches to NAMED grid areas (label / armor / mod / remove) for its two
   selects and remove button. A three-child row has no such children, so its
   cells were placed against areas they do not define and landed arbitrarily.
   The first cut also tried to share the manual row's five-column grid so the
   modifier columns would align; they cannot, because every row is its own grid
   and `1fr` and `auto` resolve per row. Chasing that alignment was the wrong
   goal: these are two different KINDS of row, one read-only and one editable,
   and each reads better on its own terms. */
.calc-member.calc-member-live {
  grid-template-columns: auto 1fr auto;
  grid-template-areas: none;
}

.calc-member.calc-member-live .calc-member-label,
.calc-member.calc-member-live .calc-member-via,
.calc-member.calc-member-live .calc-member-mod {
  grid-area: auto;
}

.calc-member.calc-member-live .calc-member-via {
  font-size: 0.75rem;
  color: var(--ash);
}

/* An absent contribution is not a number and must not be typeset as one. In the
   modifier cell's 1.5rem display face, "Unknown" was the loudest thing on the
   roster, louder than the real values beside it. Ash and body type put it where
   it belongs: legible, and quieter than any figure that actually counts. The
   party note carries the actionable half of this in bone. */
.calc-member.calc-member-live .calc-member-mod.is-unknown {
  font-family: inherit;
  font-size: 0.85rem;
  color: var(--ash);
}

/* Law 3: a character's name is primary information, so bone, not the ash used
   for the manual rows' "Explorer 1" labels, which are furniture. Names are not
   shouted either: no uppercasing a person's name. */
.calc-member.calc-member-live .calc-member-label {
  color: var(--bone);
  text-transform: none;
  letter-spacing: 0.02em;
  font-size: 0.85rem;
}

/* Also information (it qualifies the total above it), so bone rather than ash. */
.calc-party-note {
  font-size: 0.8rem;
  color: var(--bone);
  margin-top: 0.5rem;
}

/* The build stamp (2026-07-30). Tone, not information the GM acts on, so ash --
   until the moment he is asked for it, when he needs to find it and read it.
   Hence quiet but never below the 0.75rem floor, and centred under the content
   rather than tucked into a corner where a phone would cut it off. */
.build-stamp {
  font-size: 0.75rem;
  color: var(--ash);
  text-align: center;
  margin: 2.5rem 0 1rem;
  letter-spacing: 0.04em;
}

.calc-field {
  display: block;
  margin-bottom: 0.85rem;
}

.calc-field .micro-label { display: block; margin-bottom: 0.35rem; }

/* Select in the frame technique: blood wrapper, panel inner, the native
   control transparent on top. Focus recolors the ring blood-bright. */
.calc-select {
  display: block;
  padding: 1px;
}

.calc-select > .frame-inner { display: block; }

.calc-select-el {
  display: block;
  width: 100%;
  background: transparent;
  border: none;
  outline: none;
  color: var(--bone);
  font-family: var(--font-body);
  font-size: 0.95rem;
  padding: 0.5rem 0.7rem;
  cursor: pointer;
  -webkit-appearance: none;
  appearance: none;
}

.calc-select-el option { background: var(--panel); color: var(--bone); }

.calc-select:focus-within { background: var(--blood-bright); }

.calc-add,
.calc-roll {
  font-family: var(--font-display);
  font-weight: 600;
  letter-spacing: 0.04em;
  color: var(--bone);
  background: var(--panel-hi);
  clip-path: var(--chamfer-8);
  border: none;
  cursor: pointer;
  transition: color 120ms ease;
  /* Tap-target floor (Stability day sweep 2026-07-12). */
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: 2.5rem;
}

.calc-add {
  font-size: 0.9rem;
  padding: 0.45rem 0.9rem;
}

.calc-add:hover { color: var(--blood-bright); }

.calc-remove {
  font-size: 1.2rem;
  line-height: 1;
  color: var(--ash);
  background: none;
  border: none;
  cursor: pointer;
  /* Tap-target floor (Stability day sweep 2026-07-12): the bare glyph was
     ~20px; give it a finger-sized square hit area. */
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 2.4rem;
  min-height: 2.4rem;
  padding: 0;
  transition: color 120ms ease;
}

.calc-remove:hover:not(:disabled) { color: var(--blood-bright); }
.calc-remove:disabled { opacity: 0.35; cursor: default; }

.calc-totals { margin-bottom: 0.6rem; }

.calc-formula {
  font-size: 0.85rem;
  color: var(--ash);
  margin-bottom: 0.9rem;
}

.calc-roll {
  font-size: 1.05rem;
  padding: 0.6rem 1.4rem;
}

.calc-roll:hover { color: var(--blood-bright); }

.calc-result {
  display: flex;
  align-items: center;
  gap: 1.1rem;
  margin-top: 1.1rem;
  min-height: 2.4rem;
}

.calc-result:empty { display: none; }

.calc-die {
  font-family: var(--font-display);
  font-size: 3rem;
  font-weight: 700;
  line-height: 1;
  color: var(--bone);
  min-width: 2.4ch;
  text-align: center;
}

.calc-die.crit { color: var(--ember-light); }
.calc-die.fumble { color: var(--blood-bright); }

.calc-outcome { display: flex; flex-direction: column; gap: 0.15rem; }

.calc-outcome-line {
  font-size: 0.85rem;
  color: var(--ash);
  letter-spacing: 0.02em;
}

.calc-verdict {
  font-family: var(--font-display);
  font-size: 1.35rem;
  font-weight: 600;
  color: var(--bone);
}

.calc-verdict.lost { color: var(--blood-bright); }

.calc-momentum {
  font-size: 0.8rem;
  color: var(--ember-light);
  margin-top: 0.1rem;
}

@media (max-width: 620px) {
  .calc-member {
    grid-template-columns: 1fr 1fr auto auto;
    row-gap: 0.3rem;
  }
  .calc-member-label { grid-column: 1 / -1; }
}

/* Narrow phones: two side-by-side selects clip their option text
   ("Standard weapon"). Stack them full-width, with the modifier and the
   remove control spanning both rows on the right (Stability day sweep
   2026-07-12). Children are positional: 1 label, 2 armor, 3 weapon,
   4 modifier, 5 remove. */
@media (max-width: 480px) {
  .calc-member {
    grid-template-columns: 1fr auto auto;
    grid-template-areas:
      "label  label   label"
      "armor  mod     remove"
      "weapon mod     remove";
    column-gap: 0.6rem;
    row-gap: 0.4rem;
    align-items: center;
  }
  .calc-member > :nth-child(1) { grid-area: label; }
  .calc-member > :nth-child(2) { grid-area: armor; }
  .calc-member > :nth-child(3) { grid-area: weapon; }
  .calc-member > :nth-child(4) { grid-area: mod; justify-self: end; }
  .calc-member > :nth-child(5) { grid-area: remove; }
}

/* ============ LEGAL PAGES (public: /legal/) ============ */

/* Standalone public documents: no rail, no gated JS, one centered
   column of prose on a panel. Same laws as everywhere: bone carries
   information, ash is chrome, accent text below 1.2rem is blood-bright. */

.legal-stage {
  max-width: calc(70ch + 5rem);
  margin: 0 auto;
  padding: 2.5rem 1.25rem 4rem;
}

/* Wordmark, links back to the site root. */
.legal-mark {
  display: inline-block;
  font-family: var(--font-display);
  font-size: 1.3rem;
  font-weight: 600;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--bone);
  margin-bottom: 2rem;
}

.legal-panel { padding: 2rem 2rem 2.25rem; }

.legal-title {
  font-family: var(--font-display);
  font-size: 1.5rem;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--blood-bright);
  margin: 0 0 0.35rem;
}

/* Dateline under the title (chrome, not information). */
.legal-updated {
  font-size: 0.75rem;
  font-weight: 600;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--ash);
  margin: 0 0 1.6rem;
}

.legal-h {
  font-family: var(--font-display);
  font-size: 1.25rem;
  font-weight: 600;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--blood-bright);
  margin: 1.8rem 0 0.6rem;
}

/* Document index rows on the hub. */
.legal-index { list-style: none; margin: 0; padding: 0; }

.legal-index li + li { border-top: 1px solid var(--line); }

.legal-index a {
  display: block;
  padding: 0.85rem 0;
}

.legal-index .legal-doc {
  font-family: var(--font-display);
  font-size: 1.25rem;
  font-weight: 600;
  letter-spacing: 0.05em;
  color: var(--bone);
}

.legal-index a:hover .legal-doc,
.legal-index a:focus-visible .legal-doc {
  color: var(--blood-bright);
}

.legal-index .legal-doc-sub {
  display: block;
  font-size: 0.85rem;
  color: var(--ash);
  margin-top: 0.15rem;
}

/* Footer link row, shared by the legal pages and the login page. */
.legal-links {
  margin-top: 1.75rem;
  font-size: 0.85rem;
  color: var(--ash);
  text-align: center;
}

.legal-links a {
  color: var(--ash);
  text-decoration: underline dotted var(--blood);
  text-decoration-thickness: 1px;
  text-underline-offset: 3px;
}

.legal-links a:hover,
.legal-links a:focus-visible {
  color: var(--bone);
  text-decoration-color: var(--blood-bright);
}

/* --- Table Link panel (/table/) --- */
.tl-col { max-width: 40rem; margin: 0 auto; padding: 1.5rem 1rem 3rem; }
.tl-col h1 { font-family: "Cormorant Garamond", Georgia, serif; font-size: 1.9rem; color: var(--blood-bright); margin: 0 0 .3rem; }
.tl-col .panel { padding: 1rem 1.1rem; margin: 1rem 0 0; }
.tl-h { font-size: .78rem; letter-spacing: .12em; text-transform: uppercase; color: var(--blood-bright); margin: 0 0 .6rem; }
.tl-col .muted { color: var(--ash); font-size: .85rem; }
.tl-col .row { display: flex; align-items: center; justify-content: space-between; gap: .6rem; padding: .4rem 0; border-bottom: 1px solid var(--line); }
.tl-col .row:last-child { border-bottom: 0; }
.tl-notice { min-height: 1.1rem; margin: .4rem 0; font-size: .85rem; color: var(--ash); }
.tl-notice.err { color: var(--blood-bright); }
.tl-notice.ok { color: var(--ember-light); }
.tl-label { display: block; font-size: .78rem; color: var(--ash); margin: 0 0 .3rem; }
.tl-joincode { font-family: "Cormorant Garamond", Georgia, serif; font-size: 2.4rem; letter-spacing: .18em; color: var(--bone); margin: .1rem 0 .3rem; }
.tl-actions { display: flex; gap: .5rem; margin-top: .7rem; }
.tl-share-block { padding: .7rem 0; border-top: 1px solid var(--line); }
.tl-share-block:first-of-type { border-top: 0; padding-top: 0; }
.tl-input { width: 100%; box-sizing: border-box; background: var(--panel-hi); color: var(--bone); border: 1px solid var(--line); border-radius: 4px; padding: .5rem .6rem; margin: 0 0 .5rem; font: inherit; font-size: .95rem; }
.tl-input:focus { outline: none; border-color: var(--blood); }
textarea.tl-input { resize: vertical; }
.tl-btn { background: var(--blood); color: var(--bone); border: 0; border-radius: 4px; padding: .5rem .9rem; font: inherit; font-size: .9rem; cursor: pointer; }
.tl-btn:hover { filter: brightness(0.85); } /* law 6: darken, never lighten */
.tl-btn-ghost { background: transparent; color: var(--bone); border: 1px solid var(--line); border-radius: 4px; padding: .45rem .8rem; font: inherit; font-size: .85rem; cursor: pointer; }
.tl-btn-ghost:hover { border-color: var(--ash); }
.tl-btn-mini { background: transparent; color: var(--ash); border: 1px solid var(--line); border-radius: 4px; padding: .25rem .6rem; font: inherit; font-size: .78rem; cursor: pointer; }
.tl-btn-mini:hover { color: var(--blood-bright); border-color: var(--blood); }

/* ===== TABLE LINK DRAWER (global) =====
   Right-side slide-in with the GM's session + share controls, launched from
   the rail on any page. Contextual share affordances (art hover, text
   selection) light up only while a table is open (body.tl-live). */
.tl-drawer-scrim {
  position: fixed; inset: 0;
  background: rgba(0, 0, 0, 0.5);
  opacity: 0; pointer-events: none;
  transition: opacity 180ms ease;
  z-index: 60;
}
.tl-drawer-scrim.open { opacity: 1; pointer-events: auto; }

.tl-drawer {
  position: fixed; top: 0; right: 0; bottom: 0;
  width: min(420px, 92vw);
  background: var(--panel);
  border-left: 1px solid var(--line);
  box-shadow: -12px 0 40px rgba(0, 0, 0, 0.45);
  transform: translateX(100%);
  transition: transform 200ms ease;
  z-index: 61;
  display: flex; flex-direction: column;
  overflow-y: auto;
  padding: 1.1rem 1.15rem 2rem;
}
.tl-drawer.open { transform: translateX(0); }
.tl-drawer:focus { outline: none; }

.tl-drawer-head {
  display: flex; align-items: center; justify-content: space-between;
  margin-bottom: 0.4rem;
}
.tl-drawer-title {
  font-family: var(--font-display);
  font-size: 1.6rem; font-weight: 600;
  color: var(--blood-bright); margin: 0;
}
.tl-drawer-close {
  font-size: 1.6rem; line-height: 1; color: var(--ash);
  background: none; border: none; cursor: pointer;
  min-width: 2.4rem; min-height: 2.4rem;
  display: inline-flex; align-items: center; justify-content: center;
  transition: color 120ms ease;
}
.tl-drawer-close:hover { color: var(--blood-bright); }

.tl-drawer-notice {
  min-height: 1.1rem; margin: 0 0 0.5rem;
  font-size: 0.85rem; color: var(--ember-light);
}
.tl-drawer-notice.err { color: var(--blood-bright); }

.tl-drawer-body { display: flex; flex-direction: column; gap: 0.2rem; }
.tl-drawer-hint { font-size: 0.85rem; line-height: 1.5; color: var(--ash); margin: 0 0 0.9rem; }
.tl-drawer .muted { color: var(--ash); font-size: 0.85rem; }
.tl-drawer .tl-btn { margin-top: 0.4rem; }
.tl-drawer textarea.tl-input { resize: vertical; margin-bottom: 0.5rem; }

.tl-sec { padding: 0.9rem 0; border-top: 1px solid var(--line); }
.tl-sec:first-of-type { border-top: none; padding-top: 0.3rem; }

.tl-players { display: flex; flex-direction: column; gap: 0.35rem; margin-top: 0.4rem; }
.tl-player-row {
  display: flex; align-items: center; justify-content: space-between;
  gap: 0.6rem; padding: 0.35rem 0;
  border-bottom: 1px solid var(--line);
}
.tl-player-row:last-child { border-bottom: none; }
.tl-player-name { color: var(--bone); }

.tl-to { display: flex; align-items: center; gap: 0.5rem; margin: 0.5rem 0; }
.tl-select {
  flex: 1; background: var(--panel-hi); color: var(--bone);
  border: 1px solid var(--line); border-radius: 4px;
  padding: 0.4rem 0.5rem; font: inherit; font-size: 0.9rem;
}
.tl-select:focus { outline: none; border-color: var(--blood); }

/* Share affordance on the site-wide art component: hidden until a table is
   open, then revealed on hover of the plate. */
.art-panel { position: relative; }
.art-share {
  position: absolute; top: 0.5rem; right: 0.5rem;
  display: none;
  font-family: var(--font-body);
  font-size: 0.75rem; font-weight: 600;
  color: var(--bone); background: var(--blood);
  border: none; border-radius: 4px;
  padding: 0.3rem 0.55rem; cursor: pointer;
  opacity: 0; transition: opacity 120ms ease, filter 120ms ease;
  z-index: 2;
}
/* GM-012: the reveal-on-hover WAS the whole affordance, so on a touch screen
   (a stated target surface: desktop and tablet first) the button was laid out
   and clickable while permanently invisible - undiscoverable to the GM, and a
   stray tap in that corner shared a plate he never chose. Visible by default;
   the hover reveal is kept only where a hover actually exists. */
body.tl-live .art-share { display: inline-block; opacity: 1; }
@media (hover: hover) {
  body.tl-live .art-share { opacity: 0; }
  body.tl-live .art-panel:hover .art-share,
  body.tl-live .art-share:focus-visible { opacity: 1; }
}
.art-share:hover { filter: brightness(0.85); } /* law 6: darken, never lighten */

/* Floating "Share with Players" button on a text selection. */
.tl-share-float {
  position: fixed;
  display: none;
  font-family: var(--font-body);
  font-size: 0.8rem; font-weight: 600;
  color: var(--bone); background: var(--blood);
  border: none; border-radius: 4px;
  padding: 0.4rem 0.7rem; cursor: pointer;
  box-shadow: 0 4px 14px rgba(0, 0, 0, 0.4);
  z-index: 62; white-space: nowrap;
}
.tl-share-float.show { display: inline-block; }
.tl-share-float:hover { filter: brightness(0.85); } /* law 6: darken, never lighten */

/* Share feedback when the drawer is shut (GM-013). Sharing art or a text
   selection happens on the page BEHIND the drawer, so the drawer's own notice
   line reported every outcome off-screen and no share, good or bad, ever said
   anything. This speaks in its place. Information in bone (law 3); errors in
   blood-bright, never blood, below 1.2rem (law 1). Opacity only: the zero
   ambient motion rule leaves no room for a slide. */
.tl-toast {
  position: fixed;
  right: 1rem; bottom: 1rem;
  max-width: min(320px, 80vw);
  font-family: var(--font-body);
  font-size: 0.85rem; line-height: 1.4;
  color: var(--bone);
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: 4px;
  padding: 0.6rem 0.8rem;
  box-shadow: 0 4px 14px rgba(0, 0, 0, 0.45);
  opacity: 0; pointer-events: none;
  transition: opacity 120ms ease;
  z-index: 62;
}
.tl-toast.show { opacity: 1; }
.tl-toast.err { color: var(--blood-bright); }

/* ============ ENCOUNTER CONSOLE (/conflict/) ============ */

/* The GM's live battlefield: a round spine, then a grid of creature cards
   with HP trackers and attack rollers. All chamfered by clip-path (never a
   CSS border), bone for numbers (law 1), ash for chrome (law 3), hover
   signalled by blood (law 6). GM-secret data: this screen is never shared. */

/* --- Round spine --- */
.enc-round {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  margin-bottom: 0.9rem;
}
.enc-round .micro-label { margin-right: 0.1rem; }

.enc-step {
  font-family: var(--font-display);
  font-size: 1.3rem;
  line-height: 1;
  color: var(--bone);
  background: var(--panel-hi);
  clip-path: var(--chamfer-6);
  border: none;
  cursor: pointer;
  min-width: 2.4rem;
  min-height: 2.4rem;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  transition: color 120ms ease;
}
.enc-step:hover { color: var(--blood-bright); }

.enc-round-val {
  font-family: var(--font-display);
  font-size: 1.7rem;
  font-weight: 700;
  line-height: 1;
  color: var(--bone);
  min-width: 1.6ch;
  text-align: center;
}

.enc-newround {
  margin-left: 0.35rem;
  font-family: var(--font-display);
  font-size: 0.95rem;
  font-weight: 600;
  letter-spacing: 0.04em;
  color: var(--bone);
  background: var(--panel-hi);
  clip-path: var(--chamfer-8);
  border: none;
  cursor: pointer;
  padding: 0.4rem 0.9rem;
  min-height: 2.4rem;
  transition: color 120ms ease;
}
.enc-newround:hover { color: var(--blood-bright); }

.enc-beats {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem;
  margin-bottom: 0.6rem;
}
.enc-beat {
  font-size: 0.8rem;
  font-weight: 600;
  letter-spacing: 0.04em;
  color: var(--ash);
  background: var(--panel-hi);
  clip-path: var(--chamfer-6);
  border: none;
  cursor: pointer;
  padding: 0.45rem 0.7rem;
  min-height: 2.2rem;
  transition: color 120ms ease;
}
.enc-beat:hover { color: var(--bone); }
.enc-beat.active {
  color: var(--bone);
  background: var(--blood);
}
.enc-beat-note {
  font-size: 0.85rem;
  color: var(--ash);
  max-width: 70ch;
  margin: 0;
}

/* --- Add row --- */
.enc-add {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: 0.7rem;
  margin-bottom: 1.1rem;
}
.enc-field { display: block; }
.enc-field .micro-label { display: block; margin-bottom: 0.35rem; }
.enc-field .calc-select { min-width: 11rem; }
/* Count ticker: reuses the round-spine step buttons. */
.enc-qty { display: inline-flex; align-items: center; gap: 0.4rem; }
.enc-qty-val {
  font-family: var(--font-display);
  font-size: 1.3rem;
  font-weight: 700;
  color: var(--bone);
  min-width: 1.6ch;
  text-align: center;
}
.enc-addbtn {
  font-family: var(--font-display);
  font-size: 1rem;
  font-weight: 600;
  letter-spacing: 0.04em;
  color: var(--bone);
  background: var(--panel-hi);
  clip-path: var(--chamfer-8);
  border: none;
  cursor: pointer;
  padding: 0.55rem 1.1rem;
  min-height: 2.6rem;
  transition: color 120ms ease;
}
.enc-addbtn:hover { color: var(--blood-bright); }

/* --- Battlefield grid --- */
.enc-cards {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
  gap: 0.8rem;
}
.enc-empty {
  grid-column: 1 / -1;
  font-size: 0.9rem;
  color: var(--ash);
  padding: 1.2rem 0;
  margin: 0;
}

/* Card: chamfered plate with a hairline edge via the inset ::before, the
   stat-chip technique (no border + clip-path pairing). */
.enc-card {
  position: relative;
  background: var(--line);
  clip-path: var(--chamfer-8);
  padding: 0.65rem 0.8rem 0.75rem;
  transition: opacity 160ms ease;
}
.enc-card::before {
  content: "";
  position: absolute;
  inset: 1px;
  z-index: -1;
  background: var(--panel);
  clip-path: var(--chamfer-7);
}
.enc-card.down { opacity: 0.5; }

.enc-card-head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 0.5rem;
}
.enc-card-kicker {
  font-size: 0.75rem;
  font-weight: 600;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--ash);
  margin: 0 0 0.1rem;
}
.enc-card-name {
  font-family: var(--font-display);
  font-size: 1.2rem;
  font-weight: 600;
  line-height: 1.05;
  color: var(--bone);
  margin: 0;
}
.enc-card-remove {
  font-size: 1.2rem;
  line-height: 1;
  color: var(--ash);
  background: none;
  border: none;
  cursor: pointer;
  min-width: 2.2rem;
  min-height: 2.2rem;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: none;
  transition: color 120ms ease;
}
.enc-card-remove:hover { color: var(--blood-bright); }

/* Name row: creature name + Ring tag + instance count. */
.enc-card-namerow { display: flex; align-items: baseline; flex-wrap: wrap; gap: 0.4rem; }
.enc-card-ring {
  font-size: 0.68rem;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ash);
}
.enc-card-count {
  font-family: var(--font-display);
  font-size: 1rem;
  font-weight: 700;
  color: var(--blood-bright);
}

/* Instance tabs: one per creature in the group. Number + HP, tinted by
   health, struck when down, active tab in blood. Wrap freely (12 kobolds). */
.enc-tabs { display: flex; flex-wrap: wrap; gap: 0.3rem; margin-top: 0.55rem; }
.enc-tab {
  display: inline-flex;
  align-items: baseline;
  gap: 0.3rem;
  padding: 0.28rem 0.5rem;
  min-height: 2rem;
  background: var(--panel-hi);
  clip-path: var(--chamfer-6);
  border: none;
  cursor: pointer;
  transition: background 120ms ease;
}
.enc-tab-n {
  font-family: var(--font-display);
  font-size: 1rem;
  font-weight: 700;
  color: var(--bone);
  line-height: 1;
}
.enc-tab-hp { font-size: 0.72rem; font-weight: 600; line-height: 1; }
.enc-tab.green .enc-tab-hp { color: var(--health-green); }
.enc-tab.amber .enc-tab-hp { color: var(--ember-light); }
.enc-tab.red .enc-tab-hp { color: var(--blood-bright); }
.enc-tab.down { opacity: 0.45; }
.enc-tab.down .enc-tab-hp { color: var(--ash); }
.enc-tab.active { background: var(--blood); }
.enc-tab.active .enc-tab-n,
.enc-tab.active .enc-tab-hp { color: var(--bone); }

/* HP tracker: bar + readout + controls. The GM needs the exact numbers
   (unlike the player-facing Party HUD meter, which hides them). */
.enc-hp { margin-top: 0.55rem; }
.enc-hp-bar {
  height: 8px;
  background: var(--panel-hi);
  border-radius: 4px;
  overflow: hidden;
}
.enc-hp-fill {
  height: 100%;
  background: var(--blood-bright);
  border-radius: 4px;
  transition: width 160ms ease;
}
.enc-card.down .enc-hp-fill { background: var(--ash); }

.enc-hp-row {
  display: flex;
  align-items: baseline;
  gap: 0.5rem;
  margin: 0.4rem 0 0.55rem;
}
.enc-hp-text {
  font-family: var(--font-display);
  font-size: 1.15rem;
  font-weight: 600;
  color: var(--bone);
}
.enc-hp-down {
  display: none;
  font-size: 0.75rem;
  font-weight: 600;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--blood-bright);
}
.enc-card.down .enc-hp-down { display: inline; }

.enc-hp-controls {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.4rem;
}
/* The damage button is the big tap target: blood-filled, the fight's verb. */
.enc-hp-hit {
  font-family: var(--font-display);
  font-size: 0.95rem;
  font-weight: 600;
  letter-spacing: 0.03em;
  color: var(--bone);
  background: var(--blood);
  clip-path: var(--chamfer-6);
  border: none;
  cursor: pointer;
  padding: 0.4rem 0.8rem;
  min-height: 2.4rem;
  transition: filter 120ms ease;
}
.enc-hp-hit:hover:not(:disabled) { filter: brightness(0.85); } /* law 6: darken, never lighten */
.enc-hp-hit:disabled { opacity: 0.4; cursor: default; }

.enc-hp-amount {
  width: 3.2rem;
  background: var(--panel-hi);
  color: var(--bone);
  border: none;
  clip-path: var(--chamfer-6);
  padding: 0.4rem 0.5rem;
  font: inherit;
  font-size: 0.9rem;
  text-align: center;
  min-height: 2.4rem;
  box-sizing: border-box;
}
.enc-hp-amount:focus { outline: 1px solid var(--blood); }

.enc-hp-apply,
.enc-hp-heal {
  font-family: var(--font-display);
  font-size: 0.95rem;
  font-weight: 600;
  color: var(--bone);
  background: var(--panel-hi);
  clip-path: var(--chamfer-6);
  border: none;
  cursor: pointer;
  min-height: 2.4rem;
  padding: 0.4rem 0.7rem;
  transition: color 120ms ease;
}
.enc-hp-heal { min-width: 2.4rem; }
.enc-hp-apply:hover,
.enc-hp-heal:hover { color: var(--blood-bright); }

/* Which instance the HP tracker is acting on (grouped cards). */
.enc-hp-which {
  font-size: 0.72rem;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ash);
}
/* Remove just the active instance from a group. */
.enc-hp-drop {
  font-size: 0.75rem;
  color: var(--ash);
  background: none;
  border: none;
  cursor: pointer;
  min-height: 2.4rem;
  padding: 0 0.5rem;
  transition: color 120ms ease;
}
.enc-hp-drop:hover { color: var(--blood-bright); }

/* Inline stat line: no boxes. Ash label + bone value, middot-separated,
   wrapping on narrow cards. Compact by design (Threat/Damage/Speed/Init). */
.enc-stats {
  display: flex;
  flex-wrap: wrap;
  gap: 0.15rem 0.55rem;
  margin-top: 0.6rem;
  font-size: 0.8rem;
  line-height: 1.35;
}
.enc-stat { display: inline-flex; align-items: baseline; gap: 0.3rem; }
.enc-stat + .enc-stat::before {
  content: "·";
  color: var(--ash);
}
.enc-stat-k {
  font-size: 0.68rem;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ash);
}
.enc-stat-v { color: var(--bone); font-weight: 600; }

/* Attack table + roller. The rolled row is called out in blood. Notched a
   step smaller than the reading tables, held at the 0.75rem legibility floor. */
.enc-atk { margin-top: 0.65rem; }
.enc-atk-table { max-width: none; }
.enc-atk-table tbody th,
.enc-atk-table td { font-size: 0.8rem; padding: 0.28rem 0.5rem; }
.enc-atk-table thead th { padding: 0.28rem 0.5rem; }
.enc-atk-table tbody tr.hit td,
.enc-atk-table tbody tr.hit th { color: var(--blood-bright); }
.enc-atk-roll {
  display: flex;
  align-items: center;
  gap: 0.7rem;
  margin-top: 0.5rem;
  flex-wrap: wrap;
}
.enc-atk-btn {
  font-family: var(--font-display);
  font-size: 0.95rem;
  font-weight: 600;
  letter-spacing: 0.03em;
  color: var(--bone);
  background: var(--panel-hi);
  clip-path: var(--chamfer-8);
  border: none;
  cursor: pointer;
  padding: 0.45rem 0.95rem;
  min-height: 2.5rem;
  transition: color 120ms ease;
}
.enc-atk-btn:hover { color: var(--blood-bright); }
.enc-atk-out {
  font-family: var(--font-display);
  font-size: 1.15rem;
  font-weight: 600;
  color: var(--bone);
}

.enc-traits {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 0.45rem 1rem;
  margin-top: 0.65rem;
}
.enc-trait .micro-label { display: block; margin-bottom: 0.25rem; }
.enc-trait-text {
  font-size: 0.85rem;
  line-height: 1.5;
  color: var(--bone);
  margin: 0;
}
@media (max-width: 420px) {
  .enc-traits { grid-template-columns: 1fr; }
}

/* Encounter readout: count + the initiative opposition it implies. */
.enc-readout {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.6rem 1.1rem;
  margin-top: 1.1rem;
  padding-top: 0.9rem;
  border-top: 1px solid var(--line);
}
.enc-count {
  font-family: var(--font-display);
  font-size: 1.1rem;
  font-weight: 600;
  color: var(--bone);
}
.enc-opp {
  display: inline-flex;
  align-items: center;
  gap: 0.45rem;
  flex-wrap: wrap;
}
.enc-opp-label {
  font-size: 0.75rem;
  font-weight: 600;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--ash);
}
.enc-opp-val {
  font-family: var(--font-display);
  font-size: 1.1rem;
  font-weight: 600;
  color: var(--bone);
}
.enc-opp-set {
  font-size: 0.8rem;
  font-weight: 600;
  color: var(--bone);
  background: var(--panel-hi);
  clip-path: var(--chamfer-6);
  border: none;
  cursor: pointer;
  padding: 0.35rem 0.7rem;
  min-height: 2.2rem;
  transition: color 120ms ease;
}
.enc-opp-set:hover:not(:disabled) { color: var(--blood-bright); }
.enc-opp-set:disabled { color: var(--ember-light); cursor: default; }
.enc-opp-note {
  flex-basis: 100%;
  font-size: 0.78rem;
  color: var(--ash);
  margin: 0;
}
.enc-clear {
  margin-left: auto;
  font-size: 0.8rem;
  font-weight: 600;
  color: var(--ash);
  background: none;
  border: none;
  cursor: pointer;
  padding: 0.35rem 0.6rem;
  min-height: 2.2rem;
  transition: color 120ms ease;
}
.enc-clear:hover,
.enc-clear.armed { color: var(--blood-bright); }

/* GM lens on a combat-rule page: the reframed one-liner, ahead of canon. */
.enc-lens {
  font-size: 1rem;
  line-height: 1.6;
  max-width: 70ch;
  color: var(--bone);
  margin: 0;
}

/* ==========================================================================
   BOOKLETS - the in-site reader for the original booklets. A fork of cards
   (Player / Gamemaster / Adventure) that opens a paginated page-image viewer.
   Pages are gated WebP; the reader is vanilla, phone-first, zero dependency.
   ========================================================================== */

.booklets-head { margin: 0.5rem 0 1.25rem; }
.booklets-title {
  font-family: var(--font-display);
  font-size: 1.5rem;
  color: var(--blood-bright);
  margin: 0;
}
.booklets-sub { color: var(--ash); margin: 0.25rem 0 0; }

.booklets-fork {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
  gap: 1.25rem;
  align-items: start;
}

.booklet-card {
  display: flex;
  flex-direction: column;
  padding: 0;
  overflow: hidden;
  text-decoration: none;
  color: inherit;
  transition: transform 120ms ease;
}
a.booklet-card:hover { transform: translateY(-3px); }
.booklet-card.is-coming { opacity: 0.5; cursor: default; }

.booklet-cover {
  aspect-ratio: 810 / 1146;
  background: #000;
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
}
.booklet-cover img { width: 100%; height: 100%; object-fit: cover; display: block; }
.booklet-coming-mark {
  font-family: var(--font-display);
  color: var(--ash);
  font-size: 1.1rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
}

.booklet-meta {
  padding: 0.75rem 0.9rem 0.9rem;
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
}
.booklet-card-title { font-family: var(--font-display); font-size: 1.15rem; color: var(--bone); }
.booklet-card-sub { font-size: 0.8rem; color: var(--ash); }

/* Reader: fills the page box on desktop; a tall stage on phone. */
body.reading .page.booklets { padding: 0; }
.reader { display: flex; flex-direction: column; height: 100%; }

.reader-bar {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  padding: 0.5rem 0.75rem;
  background: var(--panel);
  border-bottom: 1px solid var(--line);
  flex: 0 0 auto;
}
.reader-back {
  background: none;
  border: none;
  color: var(--ash);
  font-family: var(--font-body);
  font-size: 0.9rem;
  cursor: pointer;
  padding: 0.4rem 0.5rem;
}
.reader-back:hover { color: var(--bone); }
.reader-title { display: flex; align-items: baseline; gap: 0.5rem; flex: 1; min-width: 0; }
.reader-title-name {
  font-family: var(--font-display);
  font-size: 1.05rem;
  color: var(--bone);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.reader-title-ed { font-size: 0.75rem; color: var(--ash); flex: 0 0 auto; }

.reader-nav { display: flex; align-items: center; gap: 0.35rem; flex: 0 0 auto; }
.reader-navbtn {
  background: var(--panel-hi);
  border: none;
  color: var(--bone);
  width: 2.4rem;
  height: 2.4rem;
  font-size: 1.4rem;
  line-height: 1;
  cursor: pointer;
  clip-path: var(--chamfer-6);
}
.reader-navbtn:hover { color: var(--blood-bright); }
.reader-page-input {
  width: 3.2rem;
  text-align: center;
  background: var(--ink);
  color: var(--bone);
  border: 1px solid var(--line);
  font-family: var(--font-body);
  font-size: 0.9rem;
  padding: 0.3rem;
  -moz-appearance: textfield;
}
.reader-count { color: var(--ash); font-size: 0.85rem; }

/* Continuous vertical scroll of every page — read it like a manual.
   --reader-w is the ONE knob for on-screen page size; lower it to make the
   pages smaller. Each page reserves its height via aspect-ratio (set inline
   per booklet from the manifest) so lazy images stream in without breaking
   scroll position. */
.reader-stage {
  --reader-w: 620px;
  flex: 1 1 auto;
  min-height: 60vh;
  overflow-y: auto;
  overflow-x: hidden;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 1rem;
  padding: 1rem;
  background: var(--ink);
  -webkit-overflow-scrolling: touch;
}
.reader-page-wrap {
  width: min(100%, var(--reader-w));
  aspect-ratio: 1749 / 2481; /* overridden inline per booklet */
  background: #000;
  flex: 0 0 auto;
}
.reader-page {
  width: 100%;
  height: 100%;
  object-fit: contain;
  display: block;
  user-select: none;
}

/* Phone: the whole page scrolls (no nested scroller); the bar stays put. */
@media (max-width: 900px) {
  .reader { height: auto; }
  .reader-bar { position: sticky; top: 0; z-index: 6; }
  .reader-stage { overflow: visible; min-height: 0; padding: 0.5rem; }
}

/* ============================================================ HEX MAP (A1)
   Visual polish pass, ordered 2026-08-02.

   THE CONTRAST LADDER IS THE POINT. The map brightens as it is explored, so a
   GM can tell fog from entered from Mapped at a glance, at arm's length, on a
   13-inch laptop, without reading anything:

     fog      ash stroke at low opacity, NO fill
     entered  clearer stroke, a faint fill lift off the panel
     mapped   BONE stroke, distinctly the brightest thing on the board

   Two laws govern the colour and they pull in the same direction. Law 1: ash
   for tone, bone for information -- so progress, counts and markers are bone,
   never red. Red is STATE (selection), never data. A Mapped hex should read as
   a small achievement, which is why it takes bone rather than an accent.

   NO TERRAIN COLOUR. Fill carries knowledge, not terrain. A terrain palette is
   a palette-extension decision to be ratified into the site laws separately;
   until then terrain lives in the side panel as text, where it cannot compete
   with the ladder for the same channel.

   Spacing is one scale: 8 / 16 / 24px, no ad-hoc margins.

   Chamfer law: no rule pairs a `border` with a `clip-path`. Surfaces use the
   panel technique (element background is the edge, ::before inset 1px is the
   face); form controls follow the Table Link idiom of border + 4px radius. */

.hx-col { max-width: 1400px; }

/* `hidden` MUST ACTUALLY HIDE (found 2026-08-05).

   Every layout block on this page is toggled with the `hidden` property and
   every one of them also has its `display` set by a class -- and a class
   selector outranks the UA stylesheet's `[hidden] { display: none }`. So none of
   those toggles has ever done anything: the toolbar, the stage and the seat
   strip all painted empty before the campaign data arrived, and Player View drew
   a status strip the code has been explicitly hiding since it was written.

   Found by looking at Player View and seeing a strip that `syncStatus` had
   already set `hidden` on. `.hx-empty` is absent from this list on purpose: it
   sets no `display`, so it was the one element the attribute already reached.

   Every element listed here has a verified un-hide path in hexmap.js, so making
   the attribute bite reveals nothing and hides nothing that was meant to show. */
.hx-bar[hidden], .hx-stage[hidden], .hx-seatrow[hidden], .hx-status[hidden] { display: none; }

/* The page title in Cormorant bone, per the polish order.

   FLAGGED, because this now differs from every other page: the site's `h1` is
   the body sans everywhere else (Rules, Bestiary, Conflict, all of them), and
   CLAUDE.md's typography law fixes Cormorant for SECTION headers rather than
   page titles. Scoped to this page so nothing else moves. Tomas rules whether
   the site follows the map or the map reverts to the site. */
.hx-col > h1 {
  font-family: var(--font-display);
  color: var(--bone);
  font-weight: 600;
}
.hx-intro { max-width: 65ch; }          /* long prose never spans the viewport */

/* ---------------------------------------------------------------- toolbar */

/* One row, one baseline. WHAT you are looking at and HOW you are looking at it
   sit together on the left -- campaign selector then modes (D3, 2026-08-06) --
   and the right end carries one control only. The two columns match the
   stage's columns, so the toolbar and the panels beneath it share their
   edges. */
.hx-bar {
  display: grid;
  /* The right track sizes to CONTENT, not to the side panel's 380px. With the
     pop-out button beside the three modes the group is wider than that, and a
     fixed track clipped "Player's eye" off the edge -- `width: max-content` on
     the item did not help, because the track still clamped it. The bar's RIGHT
     edge is what must line up with the panel beneath, and an auto track keeps
     that while giving the controls the room they need. */
  grid-template-columns: minmax(0, 1fr) auto;
  gap: 16px; align-items: center;
  margin: 16px 0 16px;
}
.hx-bar-left  { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; min-width: 0; }
/* 24px between the campaign selector and the modes: 8 from the flex gap plus
   16 here. The label and its select are one control and stay at 8; the modes
   are a different question about the same map and take the next step up the
   scale. Anything less and the segmented control reads as attached to the
   dropdown. */
.hx-bar-left .hx-modes { margin-left: 16px; }
/* The right group anchors to the column's RIGHT edge and grows leftward rather
   than being squeezed into 380px. It holds ONE control now -- Close Hex Map in
   the popped window, Open in its own window in-site -- and `justify-self: end`
   is what keeps that control's right edge on the side panel's right edge
   below. Verify by measurement after any change here (D4): the alignment is a
   consequence of the track, not a promise the track makes. */
/* A1: ONE ROW, FIXED HEIGHT. The bar is chrome, and chrome that grows steals
   from the canvas. 44px holds a 32px control with 6px either side and nothing
   in it is allowed to make it taller. */
.hx-bar { min-height: 44px; }
.hx-bar .hx-input, .hx-bar .hx-btn { height: 32px; padding-top: 0; padding-bottom: 0; }
/* The popped window identifies itself with a small mark inside the toolbar
   rather than a band above it. Same masked PNG the rail paints through, already
   precached, so it costs no asset and no VERSION bump.

   ALIGNED ON THE WORDMARK, NOT THE LOCKUP'S BOX (B2, 2026-08-05). The lockup is
   a dagger driven through the word: an ornament above the T, the letterforms,
   and the blade's tail below. Measured off the asset itself rather than judged
   by eye -- in the 879x564 PNG the ornament occupies rows 63-135, the wordmark
   rows 158-446 spanning x 9-871, and the tail runs to row 562. So the word is
   barely half the height of what it sits in, and centring the box put the word
   a good 14px low while the tail overflowed 28px down into the map.

   That overflow was the whole defect, and it was not a nudge: `.rail-logo-mark`
   takes `width: 100%` with `aspect-ratio: 879/564`, which DERIVES its height
   from its width and simply ignores the 26px this rule was setting on the
   wrapper. The mark rendered 54px tall in a 26px box.

   So the box is now the WORDMARK's box -- 863x289 of the asset, ratio 2.9862 --
   and the mask is scaled and offset to show that band alone. Percentages rather
   than pixels so the crop survives a change of height: at mask-size 101.854% x
   195.156% the asset is drawn 863-wide-units across the box, and the position
   percentages resolve to the -9, -158 offset that puts the wordmark's top left
   at the box's. Every edge of this element is now ink, which is what makes the
   alignment below exact instead of approximate: `align-items: center` puts the
   word on the row's axis, and the left edge IS the T. */
.hx-bar .hx-logo {
  height: 20px; width: auto; aspect-ratio: 863 / 289;
  flex: none; overflow: hidden;
  /* 16px, not 8: the logo is not part of the campaign group it stands beside. */
  margin-right: 16px;
  background-color: var(--bone);
  -webkit-mask-image: url("../TYSTNAD_Logo_White_Transparent.png");
  mask-image: url("../TYSTNAD_Logo_White_Transparent.png");
  -webkit-mask-repeat: no-repeat; mask-repeat: no-repeat;
  -webkit-mask-size: 101.854% 195.156%; mask-size: 101.854% 195.156%;
  -webkit-mask-position: 56.25% 57.454%; mask-position: 56.25% 57.454%;
}

.hx-bar-right { display: flex; align-items: center; gap: 24px; justify-self: end; }
.hx-bar-right > * { flex: none; }
.hx-bar-right .hx-btn, .hx-modes .hx-mode { white-space: nowrap; }

/* ------------------------------------------------------------ typography */

/* Headings are Cormorant in BONE. Blood-bright is reserved for the small caps
   section labels, so a heading and a label are never the same colour. */
.hx-h {
  font-family: var(--font-display); font-size: 1.5rem; color: var(--bone);
  margin: 0 0 8px; font-weight: 600;
}
.hx-h3 {
  font-family: var(--font-display); font-size: 1.15rem; color: var(--bone);
  margin: 24px 0 8px; font-weight: 600;
}
.hx-head-title {
  font-family: var(--font-display); font-size: 1.35rem; color: var(--bone);
  letter-spacing: 0.02em; font-weight: 600;
}
/* E2: FIELD LABELS ARE ASH, NOT BLOOD (ruled 2026-08-05). Law 1 holds that red
   is state and never data; a field's name is neither -- it is structure. Blood
   caps also gave the panel a second heading system competing with the Cormorant
   step titles, so two different things were shouting at the same time. What
   keeps blood caps is the commit bar's unsaved marker, `.hx-commit-state`, and
   nothing else -- `.hx-state` went with the OVERRIDDEN caption on 2026-08-06.
   This applies to the toolbar's CAMPAIGN label too, by the same reasoning: it
   names a control. */
.hx-label {
  display: block; font-size: 0.78rem; letter-spacing: 0.08em; text-transform: uppercase;
  color: var(--ash); margin: 16px 0 8px;
}
.hx-label-inline { margin: 0; }

.hx-meta  { font-size: 0.85rem; color: var(--ash); margin: 0 0 8px; }
.hx-stat  { font-size: 0.9rem;  color: var(--bone); margin: 16px 0 0; }
.hx-travel { font-size: 0.95rem; color: var(--bone); margin: 0 0 16px; }
.hx-travel-mapped { color: var(--bone); font-size: 1.05rem; }
.hx-dp { font-size: 0.85rem; color: var(--ember-light); margin: 0 0 16px; }

/* --------------------------------------------------------------- controls */

.hx-input {
  box-sizing: border-box; background: var(--panel-hi); color: var(--bone);
  border: 1px solid var(--line); border-radius: 4px;
  padding: 8px; margin: 0 0 8px; font: inherit; font-size: 0.95rem;
  width: 100%; max-width: 420px;
}
.hx-input:focus { outline: none; border-color: var(--blood); }
textarea.hx-input { resize: vertical; }
select.hx-input, .hx-bar .hx-input { width: auto; min-width: 210px; margin: 0; }

.hx-btn {
  background: var(--panel-hi); color: var(--bone);
  border: 1px solid var(--line); border-radius: 4px;
  padding: 8px 16px; font: inherit; font-size: 0.9rem; cursor: pointer;
  margin: 8px 8px 0 0;
}
.hx-btn + .hx-btn { margin-left: 0; }
.hx-btn:hover, .hx-btn:focus-visible { border-color: var(--blood); outline: none; }
.hx-btn[disabled] { opacity: 0.5; cursor: default; }
.hx-btn[disabled]:hover { border-color: var(--line); }
.hx-btn.hx-ghost { background: transparent; color: var(--ash); }
.hx-btn.hx-ghost:hover { color: var(--bone); border-color: var(--ash); }
.hx-bar .hx-btn { margin: 0; }

/* Run / Prepare / Player's eye are mutually exclusive, so they are ONE
   segmented control with a visible active state, not three siblings. */
/* Equal-width segments. Three different widths made the active fill read as a
   patch stuck on one button rather than one control with a mode selected. */
.hx-modes {
  display: grid; grid-auto-flow: column; grid-auto-columns: 1fr;
  border: 1px solid var(--line); border-radius: 4px; overflow: hidden;
}
.hx-modes .hx-mode {
  margin: 0; border: 0; border-radius: 0; background: transparent; color: var(--ash);
  padding: 8px 16px; border-right: 1px solid var(--line); text-align: center;
}
.hx-modes .hx-mode:last-child { border-right: 0; }
.hx-modes .hx-mode:hover { color: var(--bone); }
.hx-modes .hx-mode[aria-pressed="true"] {
  background: var(--blood); color: var(--bone);
}
.hx-modes .hx-mode:focus-visible { outline: 1px solid var(--blood-bright); outline-offset: -1px; }

/* "The window was blocked, so the map is here." INFORMATION, not alarm: bone
   text with an ember rule, the same idiom the paste report uses for a line that
   needs attending to without shouting. Red is state in this product and is
   never a warning label, and nothing here has actually failed -- the GM asked
   for his map and has it, in a different place. */
.hx-blocked {
  color: var(--bone); font-size: 0.9rem; max-width: 65ch;
  margin: 0 0 16px; padding-left: 8px;
  border-left: 2px solid var(--ember-strong);
}

.hx-notice { min-height: 1.4rem; font-size: 0.9rem; color: var(--bone); opacity: 0; }
.hx-notice.hx-on { opacity: 1; }
.hx-notice.hx-bad { color: var(--blood-bright); }

/* ------------------------------------------------------------------ stage */

.hx-stage { display: grid; grid-template-columns: minmax(0, 1fr) 380px; gap: 16px; align-items: start; }

/* 16px, MATCHING THE SIDE PANEL (ruled 2026-08-05). The two panels sit side by
   side with their boxes top-aligned, and they were padding 8 against 16 -- so
   their CONTENT started 8px apart and the top of the grid never lined up with
   the first line of the panel beside it. Alignment on this page is box to box
   at the outer edges and content to content inside them; this is the second
   half of that rule. It costs 16px of canvas in each dimension, which is under
   3% at the popped window's width and buys a line the eye can actually follow
   across the whole stage. */
.hx-mapwrap {
  position: relative; background: var(--line); clip-path: var(--chamfer-8);
  padding: 16px; overflow: auto; height: 78vh; z-index: 0;
}
.hx-mapwrap::before { content: ""; position: absolute; inset: 1px; z-index: -1; background: var(--panel); }
.hx-side { max-height: 78vh; overflow: auto; padding: 16px; }

/* A2: THE CANVAS TAKES EVERYTHING THE TOOLBAR AND INSPECTOR DO NOT.
   Four rings fits today. Six is 127 hexes and eight is 217, and ring depth is a
   floor rather than a target now, so this WILL be exercised. The SVG fills its
   wrapper and the viewBox does the zooming, which keeps strokes crisp at every
   scale and costs nothing per hex. */
.hx-svg { width: 100%; height: 100%; display: block; touch-action: none; }
.hx-mapwrap { overflow: hidden; }
.hx-mapwrap.is-panning .hx-svg { cursor: grabbing; }
/* A5: a stroke in progress. Ember marks what the drag has touched but has NOT
   yet written -- distinct from every state in the knowledge ladder, because it
   is not knowledge, it is an edit the GM has not finished making.

   The per-hex rule was lost in SPEC v1's CSS purge: its old selector
   (`.hx-cell.hx-painting .hx-hex`) named the retired group-and-polygon DOM, so
   it was swept as orphaned and the stroke painted with no feedback at all.
   Rebased 2026-08-07 onto the hit path, which since SPEC v1 IS the per-hex
   element the class lands on. */
.hx-mapwrap.is-painting .hx-svg { cursor: crosshair; }
.hx-mapwrap.is-pannable .hx-svg { cursor: grab; }

/* A6: the status strip. Ash for tone, bone for the numbers, and it never grows.
   0.75rem is the floor and this sits on it deliberately: ambient information
   that must be legible when looked for and invisible when not. */
/* 16px across, following the panels above it. NOT itself a panel, so this is an
   extension of the 2026-08-05 ruling rather than the ruling: the strip spans the
   full column and its two ends are the only text on the stage that would still
   be sitting on the old 8px line. Its counts read directly under the grid's left
   edge and its state under the panel's right, which is the whole point of
   putting the panels on one line. */
.hx-status {
  display: flex; gap: 24px; align-items: center;
  margin-top: 8px; padding: 0 16px; min-height: 24px;
  font-size: 0.75rem; color: var(--ash);
}
.hx-status b { color: var(--bone); font-weight: 600; }
/* `.hx-status-right` pinned the strip's own "Saved" to the far corner and went
   with it (F2, 2026-08-06). One truth, one indicator, and the commit bar is
   the one that sits beside the button that changes it. Deleted rather than
   kept warm: an unused right-hand slot in a strip is an invitation to put
   something else in the corner the GM never looks at. */

/* G4: the keyboard, admitted. Same ambient weight as the counts beside it: the
   keys are bone because they are the information, the words are ash because they
   are the gloss. It drops out below the stage breakpoint, where the strip has no
   room for it and the keyboard is not the input anyway. */
.hx-hint { display: flex; gap: 16px; align-items: center; min-width: 0; }
.hx-hint-pair { display: inline-flex; align-items: center; gap: 8px; flex: none; }
.hx-hint-what { color: var(--ash); }

/* -------------------------------------------------- THE CONTRAST LADDER */

/* Base: no fill at all. Fill is EARNED. */
/* `pointer-events: all` MAKES AN UNFILLED HEX CLICKABLE IN ITS INTERIOR.
   SVG hit-testing follows the PAINTED area, so a polygon with `fill: none` is
   targetable only on its 1px stroke -- a hairline. Sixty of a four-ring world's
   sixty-one hexes are fog, and fog has no fill by the contrast ladder's own
   design, so almost the whole map was reachable only by hitting its outline.
   Harmless while the background did nothing; the moment drag-to-pan arrived it
   meant pressing inside a hex panned the map instead of selecting it.
   This does not paint anything: the hex stays unfilled and the ladder is
   untouched. It only says the shape may be a pointer target. */
/* ============================================ THE DETAIL PANEL (SPEC v1 §8)
   The panel is CHROME and keeps the application's palette. Only the map stands
   on paper; the paper does not leak off the map surface.

   Address, tier, terrain, survey, then a block per mark. Party presence is
   absent from this because it has no source in the schema, not because it was
   forgotten. */
.hx-glyphbox { height: 42px; display: flex; align-items: center; gap: 4px; margin: 0 0 4px; }
.hx-sub { color: var(--ash); font-size: 0.9rem; margin: 0 0 14px; font-style: italic; }
.hx-dl {
  margin: 0; display: grid; grid-template-columns: auto 1fr; gap: 6px 12px;
  font-size: 0.92rem;
}
.hx-dl dt { color: var(--ash); letter-spacing: 0.05em; }
.hx-dl dd { margin: 0; color: var(--bone); }
.hx-terrline {
  margin: 14px 0 0; padding-top: 12px; border-top: 1px solid var(--line);
  color: var(--ash); font-size: 0.88rem; line-height: 1.45;
}
.hx-markrow { margin-top: 12px; padding-top: 11px; border-top: 1px solid var(--line); }
.hx-markrow-h { display: flex; align-items: baseline; gap: 8px; margin-bottom: 3px; }
.hx-markrow-n { color: var(--bone); font-size: 1rem; }
/* The kind, small caps and ash: it names a category, and a category is
   structure rather than information (E2). */
.hx-markrow-k {
  color: var(--ash); font-size: 0.75rem; letter-spacing: 0.16em; text-transform: uppercase;
}
.hx-markrow p { margin: 0; color: var(--ash); font-size: 0.88rem; line-height: 1.45; }
/* Ember, not blood: "they have not found it yet" is a fact about the campaign
   the GM is running, not an alarm. Red in this product is state. */
.hx-markrow-un { color: var(--ember-light) !important; font-style: italic; }

/* ================================================ THE MAP SURFACE (SPEC v1)
   Ratified 2026-08-06. The map is an artifact an Explorer carries, not a UI
   panel, so it stands on paper. The chrome around it keeps the application's
   own palette -- this is confined to the map surface and does not leak.

   SIX TERRAINS, LIGHTNESS-ORDERED, and the order is the point: it is what keeps
   the map legible in greyscale and in print. Measured, not chosen. L* runs
   81.0, 74.4, 68.2, 63.9, 59.9, 54.9 against paper at 88.3, so every step falls
   between 4.1 and 7.3. If two terrains prove hard to tell apart, WIDEN THE
   LIGHTNESS GAP or lean on the texture, and do not reach for saturation.

   Road and mountains are additions to the ratified five (Tomas, 2026-08-07):
   `water` had no source in the data and was dropped, and mountains is canon --
   it is the one landscape visible at 24 miles and the only landscape fact a
   player is ever sent. Forest was lightened one step from #5f8158 because the
   hex ID read 2.98:1 against it, under the 3:1 floor; at #688d60 it reads 3.48
   and the palette's widest gap closes from 9.4 to 5.0 as a side effect. */
/* DECLARED AT THE ROOT, APPLIED ONLY ON THE MAP. The first cut scoped these to
   `.hx-paper`, which is the SVG element -- and the legend, which lives in the
   panel outside it, then drew six terrain swatches with no colour at all. The
   spec confines the paper GROUND to the map surface; that is a rule about where
   the fill is painted, not about where the token is declared, and anything that
   has to name a terrain needs to be able to reach it. */
:root {
  --paper: #e4ddce;
  --edge: #a29b90;
  --edge-soft: #c0b9ad;
  --ink: #2b2724;
  --id-known: #33302c;
  --id-unknown: #8d857b;
  /* §7's party red. The spec spends it on the party's hex border, which has no
     source in the schema -- party position is not stored anywhere. Until it is,
     it marks the SELECTED hex, which is the one red thing on a paper map either
     way. Flagged as a repurpose, not a quiet substitution. */
  --party: #a8232a;
  /* The terrains have to be reachable from the legend as well as the map, which
     is the whole reason these sit at the root. */
  --t-grassland: #dcc79a;   /* L* 81.0 */
  --t-road:      #c1b6a2;   /* L* 74.4 */
  --t-hills:     #b0a49e;   /* L* 68.2 */
  --t-mountains: #a09a93;   /* L* 63.9 */
  --t-swamp:     #9a9160;   /* L* 59.9 */
  --t-forest:    #688d60;   /* L* 54.9 */
  /* PLAYER VIEW ONLY: ground the party has walked, with no claim about what
     the ground IS. The payload carries no landscape, so a map that coloured it
     by terrain would be inventing intelligence the server deliberately withheld.
     Neutral by construction -- no hue cast to read a landscape into -- and at
     L* 72.2 it sits between road and hills, so it belongs to the set without
     being any member of it. The hex ID reads 6.11:1 against it. */
  --t-known:     #b7b0a7;   /* L* 72.2 */
}

/* The address. NOT a mid-value grey on the known state: the previous map used a
   colour close in luminance to its forest and marsh fills, which made the ID
   nearly invisible on exactly the hexes a GM most needs to read. */
.hx-paper .hx-id {
  font-family: var(--font-display); font-size: 11px; font-weight: 500;
  letter-spacing: 0.05em; text-anchor: middle; pointer-events: none;
  font-variant-numeric: lining-nums tabular-nums;
  fill: var(--id-unknown);
}
.hx-paper .hx-id.is-known { fill: var(--id-known); }

.hx-paper .hx-cell { cursor: pointer; }
/* SELECTION IS DASHED INK, AND IT USED TO BE THE PARTY RED. That was a stopgap
   taken deliberately while party position had no field in the schema and §5
   layer 6 could not be drawn -- flagged at the time as a repurpose. Migration
   020 gave the party its value, and the moment it did, a selected hex and the
   party's hex were two identical red outlines on one map saying two different
   things.

   Dashed, because selection is TRANSIENT STATE and every other line on this map
   is data: the contour is what the party knows, the party border is where they
   are, the grid is the world. A dash reads as "this is the cursor, not the
   map", and it is the only dashed line here apart from the ruin glyph's ring.
   Ink rather than a new colour for the same reason -- the palette is closed. */
.hx-paper .hx-cell:focus-visible,
.hx-paper .hx-cell.is-sel {
  outline: none;
  stroke: var(--ink); stroke-width: 2.6;
  stroke-dasharray: 7 5; stroke-linejoin: round;
}

/* A5: a stroke in progress. Ember marks what the drag has touched but has NOT
   yet written -- distinct from every state in the knowledge ladder, because it
   is not knowledge, it is an edit the GM has not finished making.

   This rule was lost in SPEC v1's CSS purge: its old selector
   (`.hx-cell.hx-painting .hx-hex`) named the retired group-and-polygon DOM, so
   it was swept as orphaned and painting gave no feedback at all. Rebased
   2026-08-07 onto the hit path, which since SPEC v1 IS the per-hex element the
   class lands on -- and placed BELOW the selection rule at equal specificity,
   because the stroke may pass through the selected hex and the live gesture
   outranks the cursor. Solid, not dashed: the dash means "selection". */
.hx-paper .hx-cell.hx-painting {
  stroke: var(--ember-strong); stroke-width: 2.4; stroke-dasharray: none;
}






.hx-cell { cursor: pointer; }

/* EVERY NAME CARRIES THE HALO NOW, not just the selected hex's (2026-08-06).
   A name sits at the bottom of its hex, which is where the hex is narrowest and
   where the NEXT ROW's numbers begin 4px later, so it was crossing hex edges
   and running up against numbers with nothing between them -- it read as a
   caption stuck between two rows rather than a label belonging to a hex.

   Moving it up was tried and measured: at y-11 it collides with its own number,
   the point-of-interest pip and the progress column at once, because a name is
   as wide as the whole hex and the bottom is the only band with nothing else in
   it. So the separation is drawn instead of found. Stroke in the page's own
   background, laid UNDER the fill by paint-order, which is the same idiom the
   selected name has used since it shipped. */
/* E2: the selected hex's whole name, drawn over its neighbours rather than
   inside its own hex, because a hex 55px wide at that height cannot hold one.
   The halo is a stroke in the page's own background laid UNDER the fill
   (`paint-order`), so the name stays readable whichever fill it crosses --
   fog, Entered or the bright Mapped surface. Same 12px as every other label:
   the floor is the floor, and this earns its legibility from contrast rather
   than from size. */

/* THE SELECTED HEX'S NAME IS NO LONGER AMONG THEM. It was hidden here because
   SVG text scales with the viewBox and it measured 5.1px at the threshold --
   under half the 0.75rem floor. It is now pinned to a constant on-screen size
   in `sizeSelName`, so the reason to hide it is gone: the threshold exists to
   suppress text that has shrunk past reading, and this text no longer shrinks.
   Its size is set as an attribute per zoom, which is why no font-size lives
   here. */
/* The hex number. Ash on purpose: a coordinate the GM looks FOR, never one that
   competes with the label or the pips. GM view only (see hexmap.js hexIds). */
.hx-id       { fill: var(--ash);  text-anchor: middle; font-family: var(--font-body); font-size: 11px; pointer-events: none; }
.hx-poi      { fill: var(--ember-light); stroke: none; pointer-events: none; }
.hx-poi-un   { fill: none; stroke: var(--ember-strong); stroke-width: 1.4; }


/* ------------------------------------------------------------ side panel */

.hx-acts   { border-top: 1px solid var(--line); padding-top: 16px; margin-top: 16px; }
.hx-explore { margin-top: 16px; }
.hx-reveal { border-top: 1px solid var(--line); margin-top: 24px; padding-top: 16px; }
.hx-form   { border-top: 1px solid var(--line); padding-top: 16px; margin-top: 24px; }

/* THE MOUNTAIN GLYPH (phase B). One silhouette at two scales: small and low is
   a range seen from the next hex, large and filling the hex is a wall the party
   walked to and could not pass. Distance does that, so it needs no legend.

   A STROKE, not a fill, and that is the pencil test made literal -- this is a
   line a Cartographer draws on paper, and the screen must not diverge from the
   paper convention it exists to teach. Fill also stays reserved for the
   knowledge ladder (fog / entered / Mapped); adding a fourth would break it.

   Ash at a glimpse: the party has seen a shape on the horizon, not learned
   anything. Bone once confirmed: "there is no way through" is hard-won
   information, and information is bone (Law 1). */
.hx-mtn {
  fill: none; stroke: var(--ash); stroke-width: 1.6;
  stroke-linejoin: round; stroke-linecap: round; pointer-events: none;
}
.hx-mtn-big { stroke: var(--bone); stroke-width: 2.2; }

/* The other five landscapes, at the mountain's glimpsed weight and colour (C2).
   Ash, because a landscape symbol is the ground rather than information the
   party has won -- the same reasoning that makes a glimpsed range ash and a
   confirmed wall bone. GM-FACING ONLY: nothing in this rule is reachable from
   the player's view, which draws one landscape fact and only one, and only
   because a mountain range is visible at 24 miles when nothing else is. */
.hx-land {
  fill: none; stroke: var(--ash); stroke-width: 1.6;
  stroke-linejoin: round; stroke-linecap: round; pointer-events: none;
}

/* The paste path (phase B). A monospace textarea, because what goes in it is
   line-structured text a GM keeps in his notes, and proportional type makes a
   keyed block harder to scan for the one line that is wrong. */
.hx-paste { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 0.8rem; }
.hx-report { margin: 16px 0; }
.hx-report-line { color: var(--bone); font-size: 0.85rem; margin: 0 0 8px; }
/* Problems read as INFORMATION, not alarm: bone, like everything the GM acts
   on. Red is state in this product and never a warning label. The ember accent
   marks the line without shouting at him mid-authoring. */
.hx-report-bad {
  color: var(--bone); font-size: 0.85rem; margin: 0 0 8px;
  padding-left: 8px; border-left: 2px solid var(--ember-strong);
}

/* Prepare-mode controls (phase B). A checkbox row: the label wraps the input so
   the whole line is the hit target, which matters most on a tablet at a table.
   Bone for the text because it is information, not tone. */
.hx-check {
  display: flex; align-items: flex-start; gap: 8px;
  color: var(--bone); font-size: 0.9rem; margin: 8px 0; cursor: pointer;
}
.hx-check input { margin-top: 2px; flex: 0 0 auto; }

/* E1: THE CHECKBOXES ARE OURS NOW. Two macOS system-blue boxes were the loudest
   things in the panel and the only two that were not TYSTNAD -- system blue
   appears nowhere else in this product, so the eye went straight to them and
   read them as the important controls. Bone and blood: unchecked is the panel's
   own quiet surface, checked takes the blood fill the mode control already uses
   for "this one is on", and the mark is bone because a tick is information. */
.hx-check input[type="checkbox"] {
  appearance: none; -webkit-appearance: none;
  width: 16px; height: 16px; margin-top: 2px; flex: 0 0 auto; position: relative;
  background: var(--panel-hi); border: 1px solid var(--ash); border-radius: 3px;
  cursor: pointer;
}
.hx-check:hover input[type="checkbox"] { border-color: var(--bone); }
.hx-check input[type="checkbox"]:checked {
  background: var(--blood); border-color: var(--blood-bright);
}
/* The tick, drawn rather than typed: a glyph would depend on a font this box is
   not guaranteed to have. Zero ambient motion, so it appears, it does not
   animate in. */
.hx-check input[type="checkbox"]:checked::after {
  content: ""; position: absolute; left: 4.5px; top: 1px;
  width: 4px; height: 8px;
  border: solid var(--bone); border-width: 0 2px 2px 0;
  transform: rotate(42deg);
}
.hx-check input[type="checkbox"]:focus-visible {
  outline: 1px solid var(--blood-bright); outline-offset: 2px;
}

/* ------------------------------------------------- the panel's four steps

   D, ruled 2026-08-05. <details>, so the browser owns the disclosure, the
   keyboard and the ARIA and the stylesheet owns only how it reads.

   E7, ONE SURFACE. The hex editor used to sit in a rounded card that ended under
   "Save hex" while everything past it sat on bare panel, which drew a boundary
   around nothing in particular. The steps are hairline-separated sections on the
   panel itself; the only cards left are the point-of-interest and region ROWS,
   where the card means "one entry" rather than "one section". That is the
   distinction carrying meaning rather than being cosmetic. */
.hx-step { border-top: 1px solid var(--line); }
.hx-step:first-of-type { border-top: 0; }
.hx-step-head {
  display: flex; align-items: baseline; gap: 8px;
  padding: 16px 0 8px; cursor: pointer; list-style: none;
  font-family: var(--font-display); font-size: 1.15rem; color: var(--bone);
  font-weight: 600;
}
.hx-step-head::-webkit-details-marker { display: none; }
.hx-step-head:hover { color: var(--bone); }
.hx-step-head:focus-visible { outline: 1px solid var(--blood-bright); outline-offset: 2px; }
/* The step's number is a coordinate, not information about the hex: ash, and
   quiet enough that the title is what the eye lands on. */
.hx-step-n {
  font-family: var(--font-body); font-size: 0.75rem; color: var(--ash);
  border: 1px solid var(--line); border-radius: 3px;
  min-width: 18px; height: 18px; flex: none;
  display: inline-flex; align-items: center; justify-content: center;
  align-self: center;
}
.hx-step-t { flex: 1 1 auto; min-width: 0; }
/* A glyph, not a rotating chevron: zero ambient motion is law here. */
.hx-step-head::after {
  content: "+"; flex: none; color: var(--ash);
  font-family: var(--font-body); font-size: 0.95rem; align-self: center;
}
.hx-step[open] > .hx-step-head::after { content: "\2212"; }

/* D4: NOTHING IS HIDDEN WHILE DIRTY WITHOUT SAYING SO. Blood caps, because this
   is state and state is the one thing red is for (law 1, E2). 0.75rem sits on
   the size floor deliberately: it must be legible when looked for and invisible
   when there is nothing to say. */
.hx-step-flag {
  display: none; flex: none; align-self: center;
  font-family: var(--font-body); font-size: 0.75rem; letter-spacing: 0.08em;
  text-transform: uppercase; color: var(--blood-bright);
}
.hx-step.is-dirty > .hx-step-head > .hx-step-flag { display: inline-block; }
.hx-step-body { padding: 0 0 16px; }

/* A disclosure inside a step, for the rare thing that must be reachable without
   being offered: the difficulty override, and the add-a-point-of-interest form.
   Quieter than a step, and it carries no number because it is not one. */
.hx-sub { margin-top: 16px; }
.hx-sub-head {
  display: inline-flex; align-items: center; gap: 8px; cursor: pointer;
  list-style: none; color: var(--ash); font-size: 0.85rem; padding: 8px 0;
}
.hx-sub-head::-webkit-details-marker { display: none; }
.hx-sub-head::before { content: "+"; color: var(--ash); }
.hx-sub[open] > .hx-sub-head::before { content: "\2212"; }
.hx-sub-head:hover { color: var(--bone); }
.hx-sub-head:focus-visible { outline: 1px solid var(--blood-bright); outline-offset: 2px; }
.hx-sub-body { padding-top: 8px; }

/* E3: a field's documentation, split off the label that used to wear it in
   caps. Ash for tone; the label above it is structure and the control below it
   is the information. */
.hx-help { font-size: 0.8rem; color: var(--ash); margin: 0 0 8px; max-width: 46ch; }

/* `.hx-state` LIVED HERE and is gone (B1, ruled 2026-08-06). It rendered one
   caption, OVERRIDDEN, over a hex whose difficulty the GM had set by hand --
   which is authorship, not a fault, and blood caps read as a charge. The rule
   is deleted with its only user rather than left behind for someone to reach
   for: an unused class is an invitation to shout again. Blood in this panel is
   now exactly one thing, the commit bar's unsaved marker. */

/* A key cap. The keyboard is the difference between an hour of dropdowns and a
   few minutes, so where a control has a key, the key travels with it. */
.hx-key {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 0.75rem; color: var(--bone); background: var(--panel-hi);
  border: 1px solid var(--line); border-radius: 3px; padding: 1px 5px;
  flex: none;
}
.hx-check .hx-key { margin-left: 8px; align-self: flex-start; }

/* D1: THE CONTEXT SLOT DOES NOT SCROLL. Which hex this is, and the numbers the
   GM acts on, stay put while the steps move under them.

   Stuck at `-16px` with its own padding-top restoring the inset, rather than
   with negative horizontal margins: the panel's edge is its element background
   showing 1px around the inset ::before face, and a child bled to the padding
   box would paint over that hairline at the top two corners. Staying inside the
   horizontal padding costs nothing, because nothing renders out there anyway. */
.hx-side-head {
  position: sticky; top: -16px; z-index: 2;
  margin-top: -16px; padding: 16px 0 8px;
  background: var(--panel);
}

/* Regions. Each one is a panel in the panel language: the element background is
   the edge and ::before inset 1px is the face, so no rule pairs a border with a
   clip-path (the chamfer law, which this feature has already broken once). */
.hx-regions { margin-top: 24px; }
.hx-region {
  position: relative; background: var(--line); clip-path: var(--chamfer-6);
  padding: 8px; margin: 16px 0; z-index: 0;
}
.hx-region::before { content: ""; position: absolute; inset: 1px; z-index: -1; background: var(--panel-hi); }
.hx-region-name { color: var(--bone); font-size: 0.98rem; margin: 0 0 8px; }

/* Dissolve is destructive but recoverable-by-design (its hexes keep every
   value), so it is quiet rather than red. Red is state in this product, never
   an invitation, and never a warning label. */
.hx-btn-quiet { color: var(--ash); }

.hx-poi-row {
  position: relative; background: var(--line); clip-path: var(--chamfer-6);
  padding: 8px; margin: 8px 0; z-index: 0;
}
.hx-poi-row::before { content: ""; position: absolute; inset: 1px; z-index: -1; background: var(--panel-hi); }
.hx-poi-known { background: var(--ember-strong); }
.hx-poi-title { color: var(--bone); font-size: 0.98rem; margin: 0 0 8px; }
.hx-poi-sub   { color: var(--ash);  font-size: 0.8rem;  margin: 0 0 8px; }
.hx-poi-notes { color: var(--bone); font-size: 0.85rem; margin: 8px 0; }

/* The legend teaches the ladder for free, in the space an unselected panel
   would otherwise waste. Its own wrapper is gone: it is a step now (F5), so the
   step carries the separator and the heading that this used to. */
.hx-legend-row {
  display: grid; grid-template-columns: 40px 1fr; gap: 16px;
  align-items: center; margin: 16px 0;
}
/* The key column holds a 26px glyph or a 32px pip strip now, not a 40px hex,
   so it sizes to what it is given rather than dictating. */
.hx-legend-key { justify-self: center; }
/* A terrain swatch is the fill itself and nothing else: an outline would add a
   line weight the map does not use for terrain. */
.hx-legend-sw {
  width: 26px; height: 16px; justify-self: center; display: block;
  border: 1px solid var(--line);
}
.hx-legend-h {
  grid-column: 1 / -1; margin: 14px 0 2px; font-size: 0.72rem;
  letter-spacing: 0.22em; text-transform: uppercase; color: var(--ash);
}
.hx-legend-h:first-child { margin-top: 0; }
.hx-legend-cap {
  grid-column: 1 / -1; margin: 6px 0 0; color: var(--ash);
  font-size: 0.8rem; font-style: italic; line-height: 1.45;
}
.hx-legend-text { font-size: 0.85rem; color: var(--ash); }
.hx-legend-text b { color: var(--bone); font-weight: 600; }

.hx-empty { max-width: 520px; padding: 16px; }

/* ------------------------------------------------------------- pop-out */

/* `.page` is position:fixed and positioned by `top` and `left`, which reserve
   room for the logo and the rail. Overriding `margin-left` did nothing (it is
   fixed), so the properties actually in play are overridden here. */
body.hx-popped .page { top: 0; left: 0; padding: 8px; }
body.hx-popped .hx-col { max-width: none; height: 100%; display: flex; flex-direction: column; position: relative; }
body.hx-popped .hx-stage { flex: 1; min-height: 0; align-items: stretch; }
body.hx-popped .hx-mapwrap { height: 100%; }
body.hx-popped .hx-side { max-height: none; height: 100%; }

/* THE POPPED WINDOW'S BAND, MEASURED AND CUT (2026-08-05).

   Full-screen, the chrome above the map came to 106.4px: 8 of page padding,
   22.4 of an EMPTY notice line, 16 of bar margin, the 44px bar itself, 16 more
   below it. Two of those five are nothing a GM can see.

   The bar's own 44px is ruled (A1) and is not touched. The bar's TOP margin is:
   `.page` already holds the window edge off by 8, and a second 16 on top of
   that is the same separation stated twice. Only the popped window loses it --
   in-site the bar sits under the page title and the intro, where 16 is the
   rhythm and removing it would crowd the prose.

   WHAT THE 38px BUYS, honestly. At Fit a 61-hex world does not grow by it: the
   frame ceiling (G1 in hexmap.js) sizes the map to the panel and the map is
   then as big as it is allowed to get. Zoomed in -- which is the authoring
   state, and what the window is FOR -- every reclaimed pixel is a pixel of
   world. */
body.hx-popped .hx-bar { margin-top: 0; }

/* THE NOTICE COSTS NO HEIGHT, and still cannot shift anything.

   Its `min-height` was right for the reason it was added: the notice appears
   and vanishes, and a line that collapses when empty would jog the whole map
   down and back on every message -- worse here than anywhere, because the map's
   height is a percentage chain and a jog RE-FRAMES it. So the line is not
   collapsed. It is taken out of flow, which costs zero height and can shift
   nothing by construction.

   IT STAYS OUTSIDE THE TOOLBAR, deliberately. Parenting it into `.hx-bar-left`
   would have been tidier and would have swallowed exactly the messages that
   matter most: `.hx-bar` is `hidden` until the campaign data arrives, and a
   boot-time "No connection" fires while it still is. A notice nobody can read
   is worse than a notice that costs 22px.

   It overlays the bar's own row, in the band the bar's LEFT track occupies.
   Centring it on the window instead was the first cut and was wrong: centring
   is blind to what it centres between, so the line stayed clear of the modes at
   1440 and ran straight under them by 1280. The band is bounded on the right
   instead, by the width the right-hand group actually takes -- measured at
   503.4px for "Close Hex Map" plus the three modes, plus the bar's own 16px
   gap. THAT NUMBER IS A MEASUREMENT OF FIXED STRINGS: re-measure it if the
   button labels change, and note the failure is benign either way, an ellipsis
   slightly early or a brief overlap, never a lost message.

   Inside that band it centres, so a short message sits in the empty middle
   rather than under the campaign group. A long one is ellipsised at one line.
   It may briefly cross the campaign select, which is the right trade: for the
   3.5 seconds a notice shows, it matters more than the campaign name the GM
   picked himself. It never crosses the modes, which is where the eye goes.

   `pointer-events: none` so it can never take a click meant for a control
   beneath it -- including during a boot failure, when the bar is hidden and
   this line is the only thing on the screen. */
body.hx-popped .hx-notice {
  position: absolute; top: 0; left: 0; right: 520px;
  z-index: 3; pointer-events: none; text-align: center;
  min-height: 0; height: 44px; line-height: 44px;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}

/* `.hx-head` is GONE (A1, 2026-08-04): the popped window's identification band
   cost roughly a quarter of the window height before the map began, and a small
   mark inside the toolbar does the same job. `.hx-logo` survives because the
   toolbar uses it; it is sized there. */
.hx-logo { display: block; flex: none; }

/* ------------------------------------------------- the seat strip (in-site)

   The band is the shared party HUD component, re-parented here by hexmap.js
   and laid out by these rules alone. Scoped to `.hx-seatrow`, so no other page
   is touched: elsewhere `.band-slots` keeps its fixed two-row placement.

   The strip shares the map panel's grid column, so its left and right edges are
   the panel's edges and the two read as one composed object. */
.hx-seatrow {
  display: grid; grid-template-columns: minmax(0, 1fr) 380px;
  gap: 16px; margin: 0 0 16px;
}
.hx-seatrow .hx-band {
  position: static; width: auto; top: auto; left: auto;
  display: grid; grid-template-columns: repeat(6, minmax(0, 1fr));
  grid-template-rows: auto; grid-auto-flow: row;
  gap: 8px;
}
/* Empty seats are quiet: ash, small, no shouting caps. A filled seat lifts to
   bone. The strip should not compete with the map for attention. */
.hx-seatrow .player-slot {
  min-height: 0; padding: 8px; font-size: 0.78rem; letter-spacing: 0.04em;
  text-transform: none; justify-content: flex-start;
}
/* The caps live on the inner span, not the slot, so the override goes here.
   An empty seat should be quiet: sentence case, ash, no letterspaced shouting.
   Six of them in a row above the map must not out-shout the map. */
.hx-seatrow .player-slot-text {
  color: var(--ash); text-transform: none; letter-spacing: 0.02em;
  font-weight: 500; overflow: hidden; text-overflow: ellipsis;
}
.hx-seatrow .player-slot.is-filled { background: var(--panel-hi); }
.hx-seatrow .player-slot.is-filled .player-slot-text { color: var(--bone); }

/* ------------------------------------------- the pop-out's side panel zones

   State at the top, the legend in the middle, this pinned to the bottom. "No
   hex selected" is the resting state a GM looks at between moments, so it has
   to look composed rather than trailing off into two-thirds of nothing. */
body.hx-popped .hx-side { display: flex; flex-direction: column; }
.hx-side-foot {
  margin-top: auto; padding-top: 16px; border-top: 1px solid var(--line);
  display: flex; justify-content: space-between; align-items: baseline; gap: 16px;
}
.hx-foot-name { color: var(--bone); font-size: 0.85rem; }
/* In-site the panel is short; the NAME footer only earns its keep in the
   pop-out. The commit bar earns its keep everywhere, so it is exempt. */
body:not(.hx-popped) .hx-side-foot:not(.hx-commit) { display: none; }

/* E8: THE COMMIT BAR. One place, always the same place, holding the save control
   and the answer to "is anything unsaved" side by side. That permanent position
   used to carry the world's name and today's date -- the two things on the
   screen a GM already knew -- while the save button sat wherever the form
   happened to end and the unsaved state lived at the far bottom edge of the
   window, which is the last place anyone looks with his hands in the panel.

   Stuck to the panel's bottom by the mirror of the header's trick, so it stays
   in view however far down the steps the GM has scrolled. */
.hx-side-foot.hx-commit {
  position: sticky; bottom: -16px; z-index: 2;
  margin-top: auto; margin-bottom: -16px;
  padding: 16px 0; border-top: 1px solid var(--line);
  background: var(--panel);
  display: flex; align-items: center; justify-content: flex-start; gap: 16px;
}
.hx-commit-btn { margin: 0; flex: none; }
.hx-commit-state { font-size: 0.8rem; color: var(--ash); }
/* Blood caps, because unsaved work IS state and this is what red is for. */
.hx-commit-state.is-dirty {
  color: var(--blood-bright); font-size: 0.75rem;
  letter-spacing: 0.08em; text-transform: uppercase;
}

@media (max-width: 1000px) {
  /* The strip has no room for the keyboard hint here, and at this width the
     input is a finger rather than a keyboard anyway. */
  .hx-hint { display: none; }
  .hx-seatrow { grid-template-columns: minmax(0, 1fr); }
  .hx-seatrow .hx-band { grid-template-columns: repeat(3, minmax(0, 1fr)); }
  .hx-bar { grid-template-columns: minmax(0, 1fr); }
  .hx-bar-right { justify-content: flex-start; }
  .hx-stage { grid-template-columns: minmax(0, 1fr); }
  .hx-side { max-height: none; height: auto; }
  .hx-mapwrap { height: 60vh; }
  body.hx-popped .hx-col { display: block; height: auto; }
  body.hx-popped .hx-stage { min-height: 0; }
  body.hx-popped .hx-side { height: auto; }
  /* THE NOTICE GOES BACK INTO FLOW HERE. Above this width the bar is one row
     with an empty middle and the notice overlays it; at this width the bar has
     become two rows and the middle is gone, so an overlay would sit on top of
     the modes. Height is cheap on a narrow screen and a lost message is not. */
  body.hx-popped .hx-notice {
    position: static; text-align: left;
    min-height: 1.4rem; height: auto; line-height: inherit;
    white-space: normal; overflow: visible;
  }
  body.hx-popped .hx-bar { margin-top: 16px; }
}
