/* ══════════════════════════════════════════════════════════════════════════
   Legendary Players Guild — shared app shell
   ══════════════════════════════════════════════════════════════════════════
   One stylesheet for every page in the guild. It owns:

     · the palette          deep maroon / golden yellow / silver
     · the top navigation   anchored, static, text-only, evenly spaced
     · the page frame       exactly one viewport tall, never scrolls
     · the primitives       panels, fields, buttons, rails, sheets, tables

   Page-local <style> blocks load AFTER this file, so anything a generator
   redefines wins. Keep those blocks to genuinely page-specific things —
   canvas sizing, map keys, tooltips — and let the shell own the chrome.
   ══════════════════════════════════════════════════════════════════════════ */

:root {
  /* ── Deep maroon: the ground the whole interface sits on ─────────── */
  --maroon-abyss: #150609;   /* page backdrop, deepest recess          */
  --maroon-deep:  #24090f;   /* nav bar, sunken wells                  */
  --maroon:       #38101a;   /* panel bodies                           */
  --maroon-lit:   #4e1826;   /* raised panels, hover fills             */
  --maroon-edge:  #6b2333;   /* borders between maroon surfaces        */

  /* ── Golden yellow: every accent, every call to action ───────────── */
  --gold:         #e6c04a;
  --gold-bright:  #ffe58a;
  --gold-deep:    #a87d21;
  --gold-glow:    rgba(230, 192, 74, 0.42);

  /* ── Silver: body copy and secondary chrome ──────────────────────── */
  --silver:       #dfe3ec;   /* default text                           */
  --silver-dim:   #a7adbb;   /* labels, captions                       */
  --silver-edge:  #7d8492;   /* hairlines on silver elements           */
  --shadow:       rgba(0, 0, 0, 0.55); /* shared panel/object depth     */

  --danger:       #e2705d;

  /* ── Focus: the keyboard user's cursor ────────────────────────────
     Two-tone on purpose. The light ring holds its shape against every
     maroon surface; the dark edge ring holds it against a gold button
     fill. One pair therefore works everywhere, which is why no element
     needs a focus style of its own. */
  --focus-ring:      #ffe58a;
  --focus-ring-edge: #150609;

  /* Text drawn on top of an accent fill — gold buttons, active tabs, badges.
     Distinct from --maroon-abyss even though the dark palettes give both the
     same value: a light palette needs a light color here and a light ground
     everywhere else, and one token cannot be both. */
  --on-accent:    #150609;

  /* The dice are drawn in their own gold-to-tan palette in every theme, so
     the pips and edges need an ink that does NOT follow the accent — under a
     light palette --on-accent is white, which put white numerals on a tan die.
     Declared here in :root because parameterizeLegacyColors rewrites hex
     literals everywhere else, and this one must stay exactly what it says. */
  --die-ink:      #150609;

  /* ── Parchment: the one light surface in a dark app ────────────────
     Pricing cards, roadmap columns and the magic-item header are cream,
     not maroon, so none of the tokens above can be read on them. They
     had been borrowing the dark-ground ones anyway — gold headings on
     cream came out at 1.4:1 — and filling the gaps with one-off hex
     literals. This is that surface's own palette, and every value is
     checked against the darkest card ground it can land on.
     Deliberately theme-independent: parchment is parchment. */
  --paper:         #eee5d0;   /* card body                              */
  --paper-bright:  #f8f1df;   /* raised card, gradient top              */
  --paper-line:    #b9a678;   /* rules and card borders                 */
  --paper-ink:     #241c15;   /* body copy, prices                      */
  --paper-muted:   #4f4335;   /* captions, per-month, sub-labels        */
  --paper-gold:    #5c390b;   /* plan names, column headings            */
  --paper-off:     #5f5548;   /* a feature this tier does not include   */
  --paper-pending: #5c4a30;   /* a feature that is still being built    */
  --paper-yes:     #4f5a12;   /* the included-feature tick              */
  --paper-warn:    #7d2f14;   /* founding-price terms, scarcity         */

  /* RGB companions allow translucent UI surfaces to follow the selected
     palette without baking the default maroon or gold into rgba() values. */
  --theme-abyss-rgb: 21, 6, 9;
  --theme-deep-rgb: 36, 9, 15;
  --theme-base-rgb: 56, 16, 26;
  --theme-lit-rgb: 78, 24, 38;
  --theme-accent-rgb: 230, 192, 74;
  /* Stable aliases for components embedded in a contrasting subtree (for
     example, theme-colored controls inside a parchment panel). */
  --theme-edge: #6b2333;
  --theme-accent: #e6c04a;
  --theme-accent-bright: #ffe58a;
  --theme-accent-deep: #a87d21;
  --theme-text: #dfe3ec;
  --theme-text-dim: #a7adbb;
  --theme-text-edge: #7d8492;
  --theme-label: #150609;
  --theme-control-bg: #24090f;
  --theme-control-text: #dfe3ec;
  --theme-lightest: #e6c04a;
  --theme-darkest: #150609;

  /* ── Metrics — the single source of spacing truth ─────────────────── */
  --nav-h:        62px;
  --pad-page:     1.75rem;
  /* The widest the app is ever drawn. Past this the whole thing is centred
     and the surplus becomes margin, so a very wide monitor gets a readable
     column rather than a stretched one. */
  --page-max:     1980px;
  --panel-pad:    clamp(0.8rem, 1.35vh, 1.15rem);
  --gap:          1.25rem;
  --sidebar-width: 260px;
  --panel-opacity: 0.60;
  --body-size: 1rem;
  --section-heading-size: 1.1rem;
  --subheading-size: 1rem;
  --radius:       10px;
  --radius-sm:    6px;

  --font: "Segoe UI", "Trebuchet MS", Avenir, Arial, sans-serif;
  --font-display: "Trajan Pro", "Palatino Linotype", Georgia, serif;

  color-scheme: dark;
}

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

html { font-size: 17px; }          /* readable floor — nothing below 0.8rem */

/* The root size for narrow screens is set with the compact layout further
   down, so that it sits after this rule rather than fighting it. */

/* The app is laid out to fit one viewport and never scroll sideways —
   body.app-body already says overflow:hidden. This is the same promise on the
   root element, which body's rule does not cover: the character sheet was
   reporting a 425px document in a 390px viewport, so a phone could drag the
   whole interface 35px off-centre. `clip` rather than `hidden` because it
   does not create a scroll container, so it cannot become the containing
   block for the page's fixed overlays. */
html { overflow-x: clip; }

body {
  font-family: var(--font);
  color: var(--silver);
  /* CSS-only ambient light and a faint map-grid texture. Every layer follows
     the selected palette, and no background image asset is requested. */
  background-image:
    radial-gradient(circle at 12% 8%, rgba(var(--theme-accent-rgb), 0.14), transparent 32rem),
    radial-gradient(circle at 88% 82%, rgba(var(--theme-lit-rgb), 0.34), transparent 38rem),
    radial-gradient(ellipse at 50% 115%, rgba(var(--theme-accent-rgb), 0.08), transparent 52rem),
    repeating-linear-gradient(135deg, rgba(var(--theme-accent-rgb), 0.025) 0 1px, transparent 1px 34px),
    repeating-linear-gradient(45deg, rgba(var(--theme-accent-rgb), 0.018) 0 1px, transparent 1px 34px);
  background-position: center;
  background-size: auto;
  background-repeat: no-repeat;
  background-attachment: fixed;
  background-color: rgb(var(--theme-abyss-rgb));
}

/* The map key explains symbols that do not exist until a map is generated.
   AppShell.describeMap sets .has-map on the body when one does. */
body.app-body:not(.has-map) :is(#map-key, #building-legend, #legend-fab) {
  display: none !important;
}

input, select, button, textarea { font: inherit; color: inherit; }

/* Global button language. Individual tools still own a button's dimensions,
   layout and purpose-specific visibility, but every actual <button> shares
   the raised treatment established by the Cavern generator's Generate action.

   The opt-outs are elements that are buttons only mechanically. `.field-button`
   is the general one: a control the user reads as a form field — a custom
   dropdown, a value picker — which must look recessed, not raised. The gold
   fill means "this is the action on this screen", so a sidebar of five gold
   filter dropdowns outranks the actual primary action and reads as five
   things to press.

   `.ghost` and `.btn-ghost` are excluded because this rule was overriding
   them. Pages were already declaring their hierarchy correctly — the
   character sheet asks for one `.act.primary` (Save) and three `.act.ghost`
   (New, Load, Print) — and the blanket fill flattened all four into identical
   primaries. Secondary means secondary; it keeps its outline treatment. */
body.app-body button:not(:where([role="tab"], [data-responsive-tab], [data-medium-left-tab], [data-medium-right-tab], [data-editor-tab], [data-feature-tab], .tray-die, .die-tile, .field-button, .lib-dropdown-toggle, .ghost, .btn-ghost, .lexicon-marker, .vtt-layer-btn, #tabletop-dice button, .token-menu button, #dice-reveal, .password-reveal)) {
  border: 1px solid var(--gold-deep) !important;
  border-radius: var(--radius-sm) !important;
  color: var(--on-accent) !important;
  background: linear-gradient(180deg, var(--gold-bright) 0%, var(--gold) 55%, var(--gold-deep) 100%) !important;
  box-shadow: 0 3px 14px var(--gold-glow) !important;
  font-weight: 700 !important;
  letter-spacing: 0.05em;
  cursor: pointer;
  transition: filter 0.15s, transform 0.1s, box-shadow 0.15s !important;
}
body.app-body button:not(:where([role="tab"], [data-responsive-tab], [data-medium-left-tab], [data-medium-right-tab], [data-editor-tab], [data-feature-tab], .tray-die, .die-tile, .field-button, .lib-dropdown-toggle, .ghost, .btn-ghost, .lexicon-marker, .vtt-layer-btn, #tabletop-dice button, .token-menu button, #dice-reveal, .password-reveal)):hover:not(:disabled) {
  color: var(--on-accent) !important;
  background: linear-gradient(180deg, var(--gold-bright) 0%, var(--gold) 55%, var(--gold-deep) 100%) !important;
  filter: brightness(1.1);
  transform: translateY(-1px);
  box-shadow: 0 6px 20px var(--gold-glow) !important;
}
body.app-body button:not(:where([role="tab"], [data-responsive-tab], [data-medium-left-tab], [data-medium-right-tab], [data-editor-tab], [data-feature-tab], .tray-die, .die-tile, .field-button, .lib-dropdown-toggle, .ghost, .btn-ghost, .lexicon-marker, .vtt-layer-btn, #tabletop-dice button, .token-menu button, #dice-reveal, .password-reveal)):active:not(:disabled) {
  transform: translateY(0);
  box-shadow: 0 3px 14px var(--gold-glow) !important;
}
body.app-body button:not(:where([role="tab"], [data-responsive-tab], [data-medium-left-tab], [data-medium-right-tab], [data-editor-tab], [data-feature-tab], .tray-die, .die-tile, .field-button, .lib-dropdown-toggle, .ghost, .btn-ghost, .lexicon-marker, .vtt-layer-btn, #tabletop-dice button, .token-menu button, #dice-reveal, .password-reveal)):focus-visible {
  outline: 2px solid var(--gold-bright) !important;
  outline-offset: 2px;
}
body.app-body button:not(:where([role="tab"], [data-responsive-tab], [data-medium-left-tab], [data-medium-right-tab], [data-editor-tab], [data-feature-tab], .tray-die, .die-tile, .field-button, .lib-dropdown-toggle, .ghost, .btn-ghost, .lexicon-marker, .vtt-layer-btn, #tabletop-dice button, .token-menu button, #dice-reveal, .password-reveal)):disabled {
  opacity: 0.5 !important;
  cursor: not-allowed !important;
  filter: grayscale(0.35);
  transform: none !important;
}

/* ══════════════════════════════════════════════════════════════════════
   Focus — one ring, every interactive element, every page
   ══════════════════════════════════════════════════════════════════════
   Declared once here rather than per-component, because the failure mode
   is silent: a control with no focus style falls back to the browser's
   near-black hairline, which is invisible on maroon. `:focus-visible`
   means pointer users never see it. The `!important` is deliberate — a
   page-local stylesheet must not be able to remove the keyboard user's
   only means of knowing where they are. */
body.app-body :is(
  a[href], button, input, select, textarea, summary, label[tabindex],
  [role="button"], [role="tab"], [role="checkbox"], [role="switch"], [role="menuitem"],
  [tabindex]:not([tabindex="-1"])
):focus-visible {
  outline: 2px solid var(--focus-ring) !important;
  outline-offset: 2px !important;
  box-shadow: 0 0 0 2px var(--focus-ring-edge) !important;
}

/* ── One select treatment for the whole app ────────────────────────────
   A dropdown should look like a dropdown everywhere. Pages had been each
   inventing their own: the dungeon's settings dialog drew a caret so dim it
   read as a text input, the library filters were gold-filled and read as the
   page's primary action, and several had no caret at all. The caret follows
   the palette, so it stays visible on both dark grounds and Celestia. */
body.app-body select:not([multiple]) {
  appearance: none;
  -webkit-appearance: none;
  padding-right: 3rem;
  background-image:
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='9' viewBox='0 0 14 9'%3E%3Cpath d='m2 2 5 5 5-5' fill='none' stroke='%23e6c04a' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"),
    linear-gradient(to left, rgba(var(--theme-accent-rgb), 0.14) 0 2.45rem, transparent 2.45rem);
  background-repeat: no-repeat;
  background-position: right 0.75rem center, right center;
  background-size: 14px 9px, 100% 100%;
  cursor: pointer;
}
:root[data-theme-mode="light"] body.app-body select:not([multiple]) {
  background-image:
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='9' viewBox='0 0 14 9'%3E%3Cpath d='m2 2 5 5 5-5' fill='none' stroke='%235c390b' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"),
    linear-gradient(to left, rgba(var(--theme-accent-rgb), 0.14) 0 2.45rem, transparent 2.45rem);
}

/* Content for assistive technology only. Every page states its own name this
   way when it has no visible h1 — see ensureHeading() in app-shell.js. */
.sr-only {
  position: absolute;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

/* The first focusable element on every page. Off-screen until focused, then
   it parks itself over the nav so it is unmissable. */
.skip-link {
  position: fixed;
  top: 0;
  left: 50%;
  z-index: 2000;
  transform: translate(-50%, -120%);
  padding: 0.7rem 1.4rem;
  background: var(--gold);
  color: var(--on-accent);
  font-weight: 700;
  letter-spacing: 0.04em;
  text-decoration: none;
  border: 1px solid var(--gold-deep);
  border-top: none;
  border-radius: 0 0 var(--radius-sm) var(--radius-sm);
  box-shadow: 0 6px 20px var(--shadow);
  transition: transform 0.16s ease-out;
}
.skip-link:focus {
  transform: translate(-50%, 0);
}
@media (prefers-reduced-motion: reduce) {
  .skip-link { transition: none; }
}

/* The main region takes focus from the skip link but must not draw a ring
   for it — the visible result is the page itself scrolling into view. */
body.app-body :is(main, .app, .app-main):focus,
body.app-body :is(main, .app, .app-main):focus-visible {
  outline: none !important;
  box-shadow: none !important;
}

/* Scrollbars are suppressed app-wide: content is meant to fit one screen,
   and anything taller lives in an overlay sheet or a scroll-pane that the
   user drives with the wheel. */
* { scrollbar-width: none; -ms-overflow-style: none; }
*::-webkit-scrollbar { width: 0; height: 0; display: none; }

/* ══════════════════════════════════════════════════════════════════════
   1. Page frame — one viewport, no document scroll
   ══════════════════════════════════════════════════════════════════════ */

body.app-body {
  height: 100vh;
  height: 100dvh;
  overflow: hidden;
  display: flex;
  flex-direction: column;

  /* One gutter, used by the nav bar and by the page below it, so the brand,
     the menus, the sidebar and the identity buttons all share the same left
     and right edges. Below --page-max it is just the page padding; above it,
     it also absorbs the surplus so the content stays centred and capped.
     The bar's own background still spans the full window — only what sits
     inside it is bounded. */
  --page-gutter: max(
    var(--pad-page),
    calc((100vw - var(--page-max)) / 2 + var(--pad-page))
  );
}

/* Everything below the nav. Children scroll internally, never the page.
   The horizontal padding is the shared gutter, so the first panel's left
   edge lines up with the brand above it and the last panel's right edge
   lines up with the identity buttons. */
body.app-body > .app-main,
body.app-body > .app,
body.app-body > main {
  flex: 1 1 auto;
  min-height: 0;
  overflow: hidden;
  padding: var(--pad-page) var(--page-gutter);
  display: flex;
  align-items: stretch;
  gap: var(--gap);
}

html body.app-body h1.module-title {
  flex: 0 0 auto;
  margin: 0.7rem var(--page-gutter) 0;
  color: var(--silver) !important;
  font-family: var(--font-display);
  font-size: clamp(1.35rem, 2.2vw, 2rem);
  letter-spacing: 0.08em;
  line-height: 1.2;
  text-align: center;
}

/* Opt-in pane that may overflow its box; wheel works, no visible bar. */
.scroll-pane { overflow: auto; min-height: 0; overscroll-behavior: contain; }

/* A tab panel is always a viewport, never a reason for the document itself
   to grow. Long tab content scrolls inside the space its parent provides. */
[role="tabpanel"] {
  min-height: 0;
  max-height: 100%;
  overflow: auto;
  overscroll-behavior: contain;
}

/* ══════════════════════════════════════════════════════════════════════
   2. Navigation — anchored at the very top, flat, text only
   ══════════════════════════════════════════════════════════════════════
   Deliberately NOT fixed and NOT floating: it is the first flex child of
   the body and stays there. The inset highlights and lower shadow give the
   shared header a restrained embossed edge on every page. */

.app-nav {
  /* Named because the user menu has to drop clear of the bar's bottom edge,
     and it is anchored to a button that the padding centres. */
  --nav-pad-y: 0.7rem;

  flex: 0 0 auto;
  display: flex;
  align-items: center;
  gap: 0.5rem;
  /* Same gutter as the page below, so the brand starts where the first
     panel does and the identity buttons end where the last one does. */
  padding: var(--nav-pad-y) var(--page-gutter);
  background: var(--maroon-deep);
  border-bottom: 1px solid rgba(var(--theme-accent-rgb), 0.28);
  box-shadow:
    inset 0 1px 0 rgba(255, 235, 171, 0.12),
    inset 0 -2px 0 rgba(38, 4, 10, 0.72),
    0 5px 12px rgba(0, 0, 0, 0.48);
  /* A stacking context so the user menu can hang below the bar and over the
     page, while still sitting under the drawer and the transition veil. */
  position: relative;
  z-index: 500;
}

.guild-invitation {
  position: relative; z-index: 20; flex: 0 0 auto;
  padding: .42rem var(--page-gutter);
  border-top: 1px solid rgba(var(--theme-accent-rgb), .24);
  background: linear-gradient(90deg, rgba(var(--theme-abyss-rgb), .97), rgba(var(--theme-base-rgb), .97), rgba(var(--theme-abyss-rgb), .97));
  color: var(--silver); font-size: .72rem; line-height: 1.35; text-align: center;
  box-shadow: 0 -8px 24px rgba(0,0,0,.24);
}
.guild-invitation a {
  color: var(--gold-bright); font-weight: 800; text-underline-offset: .16em;
}
.guild-invitation a:hover { color: var(--gold); }

@media (max-width: 600px) {
  .guild-invitation { padding-block: .35rem; font-size: .66rem; }
}

.module-intro {
  flex: 0 0 auto;
  margin: 0.2rem var(--page-gutter) 0;
  color: var(--silver-dim);
  font-size: 0.82rem;
  line-height: 1.35;
  text-align: center;
}

/* Generator headings sit above a two-column workspace. Center them over the
   output pane, excluding the fixed controls column, rather than over the
   browser viewport. */
body.app-body:is(
  [data-page="dungeon"], [data-page="cavern"], [data-page="castle"],
  [data-page="city"], [data-page="kingdom"], [data-page="text"],
  [data-page="treasure"], [data-page="weather"], [data-page="puzzles"],
  [data-page="hazards-traps"], [data-page="encounter"]
) :is(h1.module-title, .module-intro) {
  margin-left: calc(var(--page-gutter) + var(--sidebar-width) + var(--gap));
  margin-right: calc(var(--page-gutter) + var(--map-heading-right-offset, 0px));
}

/* Castle and City have a legend rail to the right of the drawing. Account for
   it so their title block is centered over the map content panel itself. */
body.app-body[data-page="castle"] { --map-heading-right-offset: calc(310px + var(--gap)); }
body.app-body[data-page="city"] { --map-heading-right-offset: calc(230px + var(--gap)); }
body.app-body[data-page="city"]:has(#building-legend.wide) { --map-heading-right-offset: calc(350px + var(--gap)); }

@media (min-width: 1025px) and (max-width: 1320px) {
  body.app-body[data-page="castle"] { --map-heading-right-offset: calc(250px + var(--gap)); }
}

.early-access-banner {
  position: relative;
  z-index: 490;
  flex: 0 0 auto;
  display: grid;
  grid-template-columns: minmax(0, 1fr) 34px;
  align-items: center;
  gap: 0.75rem;
  min-height: 2.6rem;
  padding: 0.45rem var(--page-gutter);
  color: var(--silver);
  background: linear-gradient(90deg, rgba(var(--theme-base-rgb), .96), rgba(var(--theme-lit-rgb), .96));
  border-bottom: 1px solid var(--gold-deep);
  box-shadow: 0 5px 14px rgba(0, 0, 0, .3);
}
.early-access-banner p {
  margin: 0;
  text-align: center;
  font-size: 0.82rem;
  line-height: 1.35;
}
body.app-body .early-access-banner-close {
  display: inline-grid !important;
  width: 30px !important;
  height: 30px !important;
  min-width: 30px !important;
  min-height: 30px !important;
  place-items: center !important;
  padding: 0 !important;
  color: var(--silver) !important;
  background: transparent !important;
  border: 1px solid var(--silver-edge) !important;
  border-radius: var(--radius-sm) !important;
  box-shadow: none !important;
  font: 400 1.25rem/1 Arial, sans-serif !important;
  letter-spacing: 0 !important;
}
body.app-body .early-access-banner-close:hover {
  color: var(--gold-bright) !important;
  border-color: var(--gold) !important;
  background: rgba(var(--theme-abyss-rgb), .2) !important;
}

.app-nav .nav-brand {
  font-family: var(--font-display);
  font-size: 1.05rem;
  font-variant: small-caps;
  letter-spacing: 0.12em;
  color: var(--gold);
  text-shadow:
    0 1px 0 rgba(255, 239, 181, 0.22),
    0 -1px 0 rgba(45, 10, 13, 0.92),
    0 3px 5px rgba(0, 0, 0, 0.55);
  text-decoration: none;
  white-space: nowrap;
  margin-right: 1.5rem;
  text-shadow: 0 0 14px rgba(var(--theme-accent-rgb), 0.28);
}
.app-nav .nav-brand-compact { display: none; }

@media (max-width: 800px) {
  .app-nav .nav-brand-full { display: none; }
  .app-nav .nav-brand-compact { display: inline; }
}

/* The section menus are centred on the viewport. Brand and identity remain
   anchored to their respective edges and do not influence that centre. */
.app-nav .nav-links {
  position: absolute;
  left: 50%;
  transform: translateX(-50%);
  flex: 0 0 auto;
  display: flex;
  justify-content: center;
  align-items: center;
  gap: 0.4rem;
  list-style: none;
}

/* These are deliberately NOT scoped to .app-nav: below the compact
   breakpoint the links and the identity buttons are reparented into the
   drawer, and scoped rules would strip them back to raw browser defaults. */
.nav-link {
  display: block;
  padding: 0.4rem 0.9rem;
  border-radius: var(--radius-sm);
  font-size: 0.95rem;
  letter-spacing: 0.045em;
  color: var(--silver-dim);
  text-decoration: none;
  white-space: nowrap;
  background: transparent;
  border: none;
  cursor: pointer;
  transform: scale(1);
  transition: color 0.16s, background 0.16s, transform 0.16s, text-shadow 0.16s;
}

.nav-link:hover { color: var(--gold-bright); background: rgba(var(--theme-accent-rgb), 0.09); }

/* Active page: gold, bolder, and slightly larger than its neighbours. */
.nav-link.active {
  color: var(--gold);
  font-weight: 700;
  transform: scale(1.14);
  text-shadow: 0 0 16px var(--gold-glow);
  background: rgba(var(--theme-accent-rgb), 0.12);
}

/* ── The four top-level menus ──────────────────────────────────────────
   Same machinery as the user menu on the right: a button that opens a
   panel hanging below the bar. Only one is ever open. Below the compact
   breakpoint (section 8) the button becomes a heading and the panel stays
   open, because the drawer is already the menu. */

.nav-group { position: relative; }

.nav-group-btn {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  font-family: var(--font-display);
  font-variant: small-caps;
  letter-spacing: 0.1em;
}

.nav-caret { font-size: 0.7em; line-height: 1; transition: transform 0.15s; }
.nav-group.open .nav-caret { transform: rotate(180deg); }
.nav-group.open > .nav-group-btn { color: var(--gold-bright); background: rgba(var(--theme-accent-rgb), 0.09); }

/* The menu holding the current page stays marked with every panel shut, but
   without the scale the page links use — a heading that grew would shove its
   neighbours sideways every time you changed page. */
.nav-group-btn.active { transform: none; }

.nav-submenu {
  display: none;
  position: absolute;
  /* Clear the button, then the bar's own padding, so the panel starts below
     the nav rather than cutting into it. */
  top: calc(100% + var(--nav-pad-y, 0.7rem) + 0.35rem);
  left: 0;
  z-index: 60;
  min-width: 12rem;
  flex-direction: column;
  padding: 0.35rem;
  margin: 0;
  list-style: none;
  background: var(--maroon-deep);
  border: 1px solid var(--maroon-edge);
  border-radius: var(--radius-sm);
  box-shadow: 0 10px 26px rgba(0, 0, 0, 0.55);
}
.nav-group.open .nav-submenu { display: flex; }

/* Pointer users can inspect a section without spending a click. The pseudo
   element bridges the small visual gap between the heading and its panel. */
@media (min-width: 1025px) and (hover: hover) {
  .nav-group::after { content: ""; position: absolute; left: 0; right: 0; top: 100%; height: 1.2rem; }
  .nav-group:hover .nav-submenu,
  .nav-group:focus-within .nav-submenu { display: flex; }
  .nav-group:hover .nav-caret,
  .nav-group:focus-within .nav-caret { transform: rotate(180deg); }
  .nav-group:hover > .nav-group-btn,
  .nav-group:focus-within > .nav-group-btn { color: var(--gold-bright); background: rgba(var(--theme-accent-rgb), 0.09); }
}

/* A row that only means something once you are signed in — My Games and its
   like. Hidden until /api/me answers, which is why the rule is written this
   way round: a visitor never sees it flash, and a member's menu gains a row a
   moment after the bar lands rather than being rebuilt underneath them. */
.needs-sign-in { display: none; }
body.is-signed-in .needs-sign-in { display: block; }

/* Rows in a panel: full width and flush left. The bar's centred, scaling
   treatment belongs to a row of peers, not to a stacked list. */
.nav-submenu .nav-link {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 0.75rem;
  width: 100%;
  padding: 0.45rem 0.7rem;
  text-align: left;
  transform: none;
  color: var(--silver);
}
.nav-submenu .nav-link.active {
  transform: none;
  color: var(--gold);
  background: rgba(var(--theme-accent-rgb), 0.12);
}

/* A page that does not exist yet. Muted, no pointer, no hover response —
   it is here to show the shape of the app, not to be clicked. */
.nav-link.is-soon {
  color: var(--silver-dim);
  opacity: 0.45;
  cursor: default;
}
.nav-link.is-soon:hover { color: var(--silver-dim); background: transparent; }

.soon-tag {
  font-size: 0.62rem;
  font-variant: small-caps;
  letter-spacing: 0.1em;
  padding: 0.05rem 0.35rem;
  border: 1px solid var(--maroon-edge);
  border-radius: 999px;
  color: var(--gold-deep);
  opacity: 0.85;
}

.nav-aside {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  margin-left: auto;
  font-size: 0.85rem;
  color: var(--silver-dim);
  white-space: nowrap;
}
.nav-aside .who { color: var(--gold-deep); }
.nav-aside a,
.nav-aside button {
  color: var(--silver-dim);
  text-decoration: none;
  background: transparent;
  border: 1px solid var(--maroon-edge);
  border-radius: var(--radius-sm);
  padding: 0.28rem 0.7rem;
  font-size: 0.85rem;
  cursor: pointer;
  transition: color 0.15s, border-color 0.15s;
}
.nav-aside a:hover,
.nav-aside button:hover { color: var(--gold); border-color: var(--gold); }

/* ── The signed-in user's menu ─────────────────────────────────────────
   The name is the control. Account, Roster and Sign out live behind it,
   which is why none of them is a nav link or a guild hall tile. */

.user-menu { position: relative; display: flex; align-items: center; }

.nav-aside .user-menu .who {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  color: var(--silver-dim);
  max-width: 14rem;
  border: 0;
  background: transparent;
}
.nav-aside .user-menu .who:hover,
.user-menu.open .who { color: var(--gold-bright); border-color: transparent; }
.nav-aside .user-menu .who.active {
  color: var(--gold);
  border-color: var(--gold-deep);
  background: rgba(var(--theme-accent-rgb), 0.12);
}

.who-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.who-caret {
  font-size: 0.7em;
  line-height: 1;
  transition: transform 0.15s;
}
.user-menu.open .who-caret { transform: rotate(180deg); }

.user-menu-list {
  display: none;
  position: absolute;
  /* Clear the button, then the bar's own padding, so the panel starts below
     the nav rather than cutting into it. */
  top: calc(100% + var(--nav-pad-y, 0.7rem) + 0.35rem);
  right: 0;
  min-width: 11rem;
  flex-direction: column;
  gap: 0.15rem;
  padding: 0.35rem;
  background: var(--maroon-deep);
  border: 1px solid var(--maroon-edge);
  border-radius: var(--radius-sm);
  box-shadow: 0 10px 26px rgba(0, 0, 0, 0.55);
}
.user-menu.open .user-menu-list { display: flex; }

/* Flat inside the menu — the panel already carries the border. */
.nav-aside .user-menu-list a {
  display: block;
  border: none;
  border-radius: var(--radius-sm);
  padding: 0.45rem 0.7rem;
  text-align: left;
  color: var(--silver);
}
.nav-aside .user-menu-list a:hover {
  color: var(--gold);
  background: rgba(var(--theme-accent-rgb), 0.1);
}
.nav-aside .user-menu-list a.sign-out {
  margin-top: 0.15rem;
  border-top: 1px solid var(--maroon-edge);
  border-radius: 0 0 var(--radius-sm) var(--radius-sm);
  color: var(--silver-dim);
}

/* Between the compact breakpoint and roughly 1330px the bar is at its most
   crowded: the hamburger has not appeared yet, so the brand, all nine links
   and the identity buttons must share one line. Everything tightens rather
   than pushing the identity buttons off the right edge. */
@media (min-width: 1025px) and (max-width: 1340px) {
  /* Tighten the gap and the type, but never the horizontal padding — that is
     the shared gutter, and shrinking it here would pull the brand out of line
     with the panel beneath it. There is far less pressure on this row than
     when it held nine separate links. */
  .app-nav { padding: 0.7rem var(--page-gutter); gap: 0.35rem; }
  .app-nav .nav-brand { font-size: 0.9rem; letter-spacing: 0.08em; margin-right: 0.75rem; }
  .app-nav .nav-links { gap: 0.15rem; }
  .app-nav .nav-link { padding: 0.4rem 0.35rem; font-size: 0.8rem; }
  .app-nav .nav-aside { margin-left: auto; gap: 0.4rem; }
  /* The name is now the only way into the account, so it shortens rather
     than disappearing. */
  .app-nav .nav-aside .user-menu .who { max-width: 7rem; padding: 0.28rem 0.5rem; }
}

/* ══════════════════════════════════════════════════════════════════════
   3. Panels, fields, buttons — the shared vocabulary
   ══════════════════════════════════════════════════════════════════════ */

.panel {
  background: rgba(var(--theme-base-rgb), var(--panel-opacity));
  border: 1px solid var(--maroon-edge);
  border-radius: var(--radius);
  padding: var(--panel-pad);
  box-shadow: inset 0 1px 0 rgba(255, 229, 138, 0.07), 0 6px 22px rgba(0, 0, 0, 0.45);
}

.settings-bar,
#map-key,
body[data-page="dungeon"] .panel,
body[data-page="cavern"] .panel,
body[data-page="castle"] .panel,
body[data-page="city"] .panel,
body[data-page="kingdom"] .panel {
  background: rgba(var(--theme-base-rgb), var(--panel-opacity));
  border: 1px solid var(--maroon-edge);
  border-radius: var(--radius);
  padding: var(--panel-pad);
  box-shadow: inset 0 1px 0 rgba(255, 229, 138, 0.07), 0 6px 22px rgba(0, 0, 0, 0.45);
}

.panel-title,
.settings-bar h3,
#map-key h3 {
  font-family: var(--font-display);
  font-size: 0.9rem;
  font-variant: small-caps;
  letter-spacing: 0.12em;
  color: var(--gold);
  margin-bottom: 0.6rem;
}

body:is([data-page="dungeon"],[data-page="cavern"],[data-page="castle"],[data-page="city"]) .sidebar {
  padding: var(--panel-pad);
  background: rgba(var(--theme-base-rgb), var(--panel-opacity));
  border: 1px solid var(--maroon-edge);
  border-radius: var(--radius);
  box-shadow: inset 0 1px 0 rgba(255, 229, 138, 0.07), 0 6px 22px rgba(0, 0, 0, 0.45);
}
body:is([data-page="dungeon"],[data-page="cavern"],[data-page="castle"],[data-page="city"]) .sidebar > .settings-bar,
body:is([data-page="dungeon"],[data-page="cavern"],[data-page="castle"],[data-page="city"]) .sidebar > #map-key {
  flex: 0 0 auto;
  width: 100%;
  padding: 0;
  background: none;
  border: 0;
  border-radius: 0;
  box-shadow: none;
}
body:is([data-page="dungeon"],[data-page="cavern"],[data-page="castle"],[data-page="city"]) .sidebar > #map-key {
  min-height: 0;
  overflow-y: auto;
  overscroll-behavior: contain;
}

/* Map-generator sidebars have one outer surface. The action stack stays flat
   inside it, while the map key remains the sole inset panel. This deliberately
   does not alter or relocate any configuration dialog content. */
body.app-body:is([data-page="dungeon"],[data-page="cavern"],[data-page="castle"],[data-page="city"],[data-page="kingdom"]) .sidebar {
  gap: clamp(0.85rem, 1.6vh, 1.1rem);
  padding: var(--panel-pad);
  background: rgba(var(--theme-base-rgb), var(--panel-opacity));
  border: 1px solid var(--maroon-edge);
  border-radius: var(--radius);
  box-shadow: inset 0 1px 0 rgba(255, 229, 138, 0.07), 0 6px 22px rgba(0, 0, 0, 0.45);
}

body.app-body:is([data-page="dungeon"],[data-page="cavern"],[data-page="castle"],[data-page="city"],[data-page="kingdom"]) .sidebar > .settings-bar {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: 0.7rem;
  width: 100%;
  padding: 0 !important;
  margin: 0;
  background: none !important;
  border: 0 !important;
  border-radius: 0 !important;
  box-shadow: none !important;
}

body.app-body:is([data-page="dungeon"],[data-page="cavern"],[data-page="castle"],[data-page="city"],[data-page="kingdom"]) .settings-bar > :is(button, #save-wrapper) {
  width: 100%;
  min-width: 0;
  margin: 0;
}

body.app-body:is([data-page="dungeon"],[data-page="cavern"],[data-page="castle"],[data-page="city"],[data-page="kingdom"]) .settings-bar > button,
body.app-body:is([data-page="dungeon"],[data-page="cavern"],[data-page="castle"],[data-page="city"],[data-page="kingdom"]) .settings-bar > #save-wrapper > button {
  min-height: 2.65rem;
  padding: 0.65rem 0.85rem !important;
  justify-content: center;
}

body.app-body:is([data-page="dungeon"],[data-page="cavern"],[data-page="castle"],[data-page="city"]) .sidebar > #map-key {
  width: 100%;
  padding: var(--panel-pad) !important;
  margin: 0;
  background: rgba(var(--theme-base-rgb), var(--panel-opacity)) !important;
  border: 1px solid var(--maroon-edge) !important;
  border-radius: var(--radius) !important;
  box-shadow: inset 0 1px 0 rgba(255, 229, 138, 0.07), 0 6px 22px rgba(0, 0, 0, 0.3) !important;
}

/* Fey Magic uses one translucent Deep Purple surface instead of a two-color
   panel gradient. Accent and text colors still come from the theme tokens. */
html[data-color-theme="purple-lilac"] body.app-body .panel,
html[data-color-theme="purple-lilac"] body.app-body .settings-bar,
html[data-color-theme="purple-lilac"] body.app-body #map-key,
html[data-color-theme="purple-lilac"] body.app-body #building-legend,
html[data-color-theme="purple-lilac"] body.app-body #sheet,
html[data-color-theme="purple-lilac"] body.app-body .sheet,
html[data-color-theme="purple-lilac"] body.app-body .settings-modal-content {
  background: rgba(var(--theme-deep-rgb), var(--panel-opacity)) !important;
}
html[data-color-theme="purple-lilac"] body:is([data-page="dungeon"],[data-page="cavern"],[data-page="castle"],[data-page="city"]) .sidebar > .settings-bar,
html[data-color-theme="purple-lilac"] body:is([data-page="dungeon"],[data-page="cavern"],[data-page="castle"],[data-page="city"]) .sidebar > #map-key {
  background: none !important;
}

/* Fey Magic reserves green-yellow for major titles. Section headings and
   field-level headings use the theme's cyan body color. */
html[data-color-theme="purple-lilac"] body.app-body h1,
html[data-color-theme="purple-lilac"] body.app-body .npc-title h2,
html[data-color-theme="purple-lilac"] body.app-body .kingdom-name {
  color: #b6ff5c !important;
}
html[data-color-theme="purple-lilac"] body.app-body h2,
html[data-color-theme="purple-lilac"] body.app-body h3,
html[data-color-theme="purple-lilac"] body.app-body h4,
html[data-color-theme="purple-lilac"] body.app-body h5,
html[data-color-theme="purple-lilac"] body.app-body h6,
html[data-color-theme="purple-lilac"] body.app-body .panel-title,
html[data-color-theme="purple-lilac"] body.app-body .field > label,
html[data-color-theme="purple-lilac"] body.app-body legend,
html[data-color-theme="purple-lilac"] body.app-body dt {
  color: #00e5ff !important;
}
html[data-color-theme="purple-lilac"] body.app-body .npc-title h2 {
  color: #b6ff5c !important;
}

/* Celestia used to need some sixty lines of overrides here, because its
   palette set black text without lightening the surfaces underneath. The
   palette now inverts properly (see COLOR_THEMES in app-shell.js) and carries
   its own --on-accent and focus pair, so every rule below this point resolves
   correctly through the tokens. Nothing theme-specific is required — and if a
   component ever does need it, that is a sign the component is painting a
   color it should be inheriting. */

.generator-controls-column {
  flex: 0 0 var(--sidebar-width);
  width: var(--sidebar-width);
  min-width: 0;
  min-height: 0;
  overflow-y: auto;
}
.generator-controls-column .options {
  display: flex;
  flex-direction: column;
  font-size: calc(1rem + 2px);
  align-items: stretch;
  gap: 0.75rem;
}
.generator-controls-column .options .field,
.generator-controls-column .options .field select,
.generator-controls-column .options .field input,
.generator-controls-column .options input,
.generator-controls-column .options button { width: 100%; min-width: 0; }
@media (max-width: 900px) {
  .generator-controls-column { flex: 0 0 auto; width: 100%; overflow: visible; }
}

/* Sidebars take whatever height is left and scroll inside themselves, so a
   long control stack never pushes the page taller than the screen. Width is
   fixed here rather than left to each page, so the first column lines up
   at the same place everywhere — a page overrides it only for the mobile
   full-width case, never for a different desktop number. */
.sidebar {
  flex: 0 0 auto;
  width: var(--sidebar-width);
  display: flex;
  flex-direction: column;
  gap: var(--gap);
  min-height: 0;
  overflow-y: auto;
  overscroll-behavior: contain;
}

/* ── Action buttons ────────────────────────────────────────────────────
   The magic item designer grew these first and every page since has written
   `class="act primary"` as though they were shared, so here they are, in the
   shell's own tokens. The designer keeps its private copy — a page's own
   stylesheet loads after this one and still wins — so promoting them changes
   nothing there while giving every other page the same buttons. */

.btn-row { display: flex; gap: 0.55rem; margin-top: 0.55rem; flex-wrap: wrap; }

button.act {
  flex: 1 1 8rem;
  border: 1px solid transparent;
  padding: 0.5rem 0.9rem;
  min-height: 2.4rem;
  font-size: 0.88rem;
  font-weight: 700;
  border-radius: var(--radius-sm);
  cursor: pointer;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  transition: background 0.15s, color 0.15s, border-color 0.15s, transform 0.1s;
}
button.act.primary { background: var(--gold); color: var(--on-accent); border-color: var(--gold); }
button.act.primary:hover { background: var(--gold-bright); border-color: var(--gold-bright); transform: translateY(-1px); }
button.act.ghost { background: transparent; color: var(--gold); border-color: var(--gold-deep); }
button.act.ghost:hover { background: var(--gold); color: var(--on-accent); border-color: var(--gold); }
button.act:disabled { opacity: 0.4; cursor: not-allowed; transform: none; }

.field { display: flex; flex-direction: column; gap: 0.3rem; }
.field label {
  font-size: 0.8rem;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  color: var(--silver-dim);
}
.field select,
.field input[type="text"],
.field input[type="number"],
.field input[type="password"],
.field input[type="email"] {
  background: var(--maroon-deep);
  color: var(--silver);
  border: 1px solid var(--maroon-edge);
  border-radius: var(--radius-sm);
  padding: 0.45rem 0.7rem;
  font-size: 0.92rem;
  width: 100%;
  transition: border-color 0.15s, box-shadow 0.15s;
}
/* Pointer focus gets the soft gold border; the keyboard ring is added by the
   shared :focus-visible rule above and must not be suppressed here. */
.field select:focus,
.field input:focus {
  border-color: var(--gold);
  box-shadow: 0 0 0 2px rgba(var(--theme-accent-rgb), 0.18);
}
.field select option { background: var(--maroon); }
.field.check { flex-direction: row; align-items: center; gap: 0.55rem; }
.field.check input, .checkbox-field input[type="checkbox"] {
  accent-color: var(--gold); width: 17px; height: 17px; cursor: pointer;
}
.checkbox-field {
  display: flex; align-items: center; gap: 0.55rem;
  font-size: 0.9rem; color: var(--silver); cursor: pointer;
}

/* Primary action — the Generate button on every generator. */
.btn-primary,
#generate-btn {
  background: linear-gradient(180deg, var(--gold-bright) 0%, var(--gold) 55%, var(--gold-deep) 100%);
  color: var(--on-accent);
  border: 1px solid var(--gold-deep);
  border-radius: var(--radius-sm);
  padding: 0.6rem 1.1rem;
  font-weight: 700;
  font-size: 0.92rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  width: 100%;
  cursor: pointer;
  box-shadow: 0 3px 14px rgba(var(--theme-accent-rgb), 0.24);
  transition: filter 0.15s, transform 0.1s, box-shadow 0.15s;
}
.btn-primary:hover:not(:disabled),
#generate-btn:hover:not(:disabled) {
  filter: brightness(1.1);
  transform: translateY(-1px);
  box-shadow: 0 6px 20px var(--gold-glow);
}
.btn-primary:disabled, #generate-btn:disabled { opacity: 0.5; cursor: not-allowed; }

/* One treatment for the main action stack in every map generator. Important
   declarations intentionally supersede older page-local ID rules while
   leaving compact zoom, tab, close, and menu-item buttons untouched. */
button.map-action {
  display: flex;
  align-items: center !important;
  justify-content: center !important;
  gap: 0.5rem !important;
  width: 100% !important;
  padding: 0.6rem 1.1rem !important;
  border: 1px solid var(--gold-deep) !important;
  border-radius: var(--radius-sm) !important;
  color: var(--on-accent) !important;
  background: linear-gradient(180deg, var(--gold-bright) 0%, var(--gold) 55%, var(--gold-deep) 100%) !important;
  box-shadow: 0 3px 14px var(--gold-glow) !important;
  font-size: 0.92rem !important;
  font-weight: 700 !important;
  line-height: 1.2 !important;
  letter-spacing: 0.08em !important;
  text-transform: uppercase !important;
  cursor: pointer !important;
  transition: filter 0.15s, transform 0.1s, box-shadow 0.15s !important;
}
button.map-action:hover:not(:disabled) {
  background: linear-gradient(180deg, var(--gold-bright) 0%, var(--gold) 55%, var(--gold-deep) 100%) !important;
  filter: brightness(1.1) !important;
  transform: translateY(-1px) !important;
  box-shadow: 0 6px 20px var(--gold-glow) !important;
}
button.map-action:active:not(:disabled) {
  transform: translateY(0) !important;
  box-shadow: 0 3px 14px var(--gold-glow) !important;
}
button.map-action:disabled { opacity: 0.5 !important; cursor: not-allowed !important; }

.btn-ghost,
#save-btn,
#copy-btn {
  background: transparent;
  color: var(--gold);
  border: 1px solid var(--gold-deep);
  border-radius: var(--radius-sm);
  padding: 0.5rem 1rem;
  font-size: 0.88rem;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  width: 100%;
  cursor: pointer;
  transition: background 0.15s, color 0.15s;
}
.btn-ghost:hover, #save-btn:hover, #copy-btn:hover { background: var(--gold); color: var(--on-accent); }

.btn-danger {
  background: transparent; color: var(--danger);
  border: 1px solid var(--danger); border-radius: var(--radius-sm);
  padding: 0.4rem 0.9rem; font-size: 0.85rem; cursor: pointer;
  transition: background 0.15s, color 0.15s;
}
.btn-danger:hover { background: var(--danger); color: #1a0508; }

/* ══════════════════════════════════════════════════════════════════════
   4. Rail — the submenu bar of extra controls, collapsible
   ══════════════════════════════════════════════════════════════════════
   Sits inside a working area and holds the secondary buttons that would
   otherwise lengthen the sidebar. Each rail button can open a sheet. */

.rail {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.4rem 0;
  flex-wrap: wrap;
}
.rail-btn {
  background: var(--maroon);
  color: var(--silver);
  border: 1px solid var(--maroon-edge);
  border-radius: var(--radius-sm);
  padding: 0.38rem 0.85rem;
  font-size: 0.87rem;
  letter-spacing: 0.04em;
  cursor: pointer;
  transition: color 0.15s, border-color 0.15s, background 0.15s;
}
.rail-btn:hover, .rail-btn[aria-expanded="true"] {
  color: var(--gold); border-color: var(--gold); background: var(--maroon-lit);
}
.rail-btn.tuck { margin-left: auto; }   /* the "hide" affordance */

.rail-collapsed .rail-body { display: none; }

/* ══════════════════════════════════════════════════════════════════════
   5. Sheet — overlaid panel; never lengthens the page
   ══════════════════════════════════════════════════════════════════════ */

.sheet-scrim {
  position: fixed; inset: 0; z-index: 900;
  background: rgba(var(--theme-abyss-rgb), 0.72);
  backdrop-filter: blur(3px);
  display: none;
  align-items: center;
  justify-content: center;
  padding: calc(var(--nav-h) + 1rem) var(--pad-page) var(--pad-page);
}
.sheet-scrim.open { display: flex; animation: sheet-in 0.22s ease-out; }

.sheet {
  background: linear-gradient(180deg, var(--maroon-lit) 0%, var(--maroon) 100%);
  border: 1px solid var(--gold-deep);
  border-radius: var(--radius);
  box-shadow: 0 18px 60px rgba(0, 0, 0, 0.7), 0 0 40px rgba(var(--theme-accent-rgb), 0.1);
  width: min(900px, 100%);
  max-height: 100%;
  display: flex;
  flex-direction: column;
  overflow: hidden;
}
/* Scoped to .sheet-scrim on purpose: the kingdom generator has its own
   long-standing .sheet-head / .sheet-body for the parchment it renders. */
.sheet-scrim .sheet-head {
  flex: 0 0 auto;
  display: flex; align-items: center; justify-content: space-between;
  gap: 1rem; padding: 0.9rem var(--panel-pad);
  background: var(--maroon-deep);
}
.sheet-scrim .sheet-head h2 {
  font-family: var(--font-display); font-variant: small-caps;
  letter-spacing: 0.1em; font-size: 1.1rem; color: var(--gold); font-weight: 600;
}
.sheet-close {
  background: transparent; border: 1px solid var(--maroon-edge);
  color: var(--silver-dim); border-radius: var(--radius-sm);
  padding: 0.25rem 0.7rem; cursor: pointer; font-size: 0.9rem;
}
.sheet-close:hover { color: var(--gold); border-color: var(--gold); }
.sheet-scrim .sheet-body { flex: 1 1 auto; min-height: 0; overflow: auto; padding: var(--panel-pad); }

@keyframes sheet-in { from { opacity: 0; } to { opacity: 1; } }

/* ══════════════════════════════════════════════════════════════════════
   5b. Settings dialog — the tabbed, never-scrolling generator dialog
   ══════════════════════════════════════════════════════════════════════
   Shared by the dungeon, cavern and castle generators. One dialog, a strip
   of tabs, and exactly one pane in the layout at a time, so the whole thing
   always fits the screen without scrolling however small the viewport is.

   Markup contract (app-shell.js wires the behaviour):

     <div class="settings-modal" id="x-settings">
       <div class="settings-modal-content">
         <div class="settings-modal-head">
           <h2>Title</h2>
           <button class="settings-modal-close" data-modal-close>&times;</button>
         </div>
         <div class="settings-tabs" role="tablist">
           <button class="settings-tab" data-tab="one" aria-selected="true">One</button>
           …
         </div>
         <div class="settings-panes">
           <div class="settings-section active" data-pane="one">…</div>
           …
         </div>
         <div class="settings-modal-footer"><button data-modal-close>Done</button></div>
       </div>
     </div>

   The scrim always keeps a margin so there is somewhere to press to dismiss. */

.settings-modal {
  display: none;
  position: fixed;
  inset: 0;
  z-index: 2000;
  background: rgba(var(--theme-abyss-rgb), 0.72);
  backdrop-filter: blur(3px);
  align-items: center;
  justify-content: center;
  padding: 4vh 4vw;                       /* the press-outside margin */
}
.settings-modal.open { display: flex; }

.settings-modal-content {
  background: linear-gradient(180deg, rgba(var(--theme-lit-rgb), 0.65) 0%, rgba(var(--theme-base-rgb), 0.65) 100%);
  border: 1px solid var(--gold-deep);
  border-radius: var(--radius);
  box-shadow: 0 18px 60px rgba(0, 0, 0, 0.75), 0 0 40px rgba(var(--theme-accent-rgb), 0.1);
  width: min(1000px, 100%);
  max-height: 100%;
  display: flex;
  flex-direction: column;
  overflow: hidden;                       /* panes fit; the dialog never scrolls */
}

.settings-modal-head {
  flex: 0 0 auto;
  display: flex; align-items: center; justify-content: space-between;
  gap: 1rem; padding: 0.85rem 1.25rem;
  background: rgba(var(--theme-deep-rgb), 0.65);
}
.settings-modal-content h2 {
  font-family: var(--font-display);
  color: var(--gold);
  font-size: 1.1rem;
  letter-spacing: 0.1em;
  font-variant: small-caps;
  text-transform: none;
  margin: 0;
}
.settings-modal-close {
  display: inline-grid !important;
  width: 34px !important;
  height: 34px !important;
  min-width: 34px !important;
  min-height: 34px !important;
  place-items: center !important;
  padding: 0 !important;
  background: transparent;
  color: var(--silver-dim);
  border: 1px solid var(--maroon-edge);
  border-radius: var(--radius-sm);
  font-family: Arial, sans-serif !important;
  font-size: 1.3rem !important;
  font-weight: 400 !important;
  line-height: 1 !important;
  text-align: center !important;
  cursor: pointer; flex-shrink: 0;
  transition: color 0.15s, border-color 0.15s;
}
.settings-modal-close:hover { color: var(--gold); border-color: var(--gold); }

/* ── The contribution amount picker ────────────────────────────────────
   Built by app-shell.js and shared by every Contribute button in the app.
   Two tiles rather than a form: there are exactly two amounts, and a
   contribution should be one press away from the button that opened this. */
.contribute-modal .contribute-modal-content { width: min(34rem, 100%); }
.contribute-modal-body {
  display: grid;
  gap: 1rem;
  padding: 1.25rem;
}
.contribute-lead {
  margin: 0;
  color: var(--silver);
  font-size: 0.9rem;
  line-height: 1.55;
}
.contribute-options {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 0.75rem;
}
.contribute-option {
  display: grid;
  align-content: center;
  justify-items: center;
  gap: 0.15rem;
  padding: 1.1rem 0.85rem;
  border: 1px solid var(--maroon-edge);
  border-radius: var(--radius);
  background: rgba(var(--theme-abyss-rgb), 0.35);
  color: var(--silver);
  text-align: center;
  text-decoration: none;
  transition: border-color 0.15s, background 0.15s, transform 0.1s;
}
.contribute-option strong {
  color: var(--gold-bright);
  font: 700 1.9rem/1 var(--font-display);
}
.contribute-option span {
  font-size: 0.72rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
}
.contribute-option small {
  color: var(--silver-dim);
  font-size: 0.72rem;
  line-height: 1.35;
}
.contribute-option:hover,
.contribute-option:focus-visible {
  border-color: var(--gold);
  background: rgba(var(--theme-accent-rgb), 0.1);
  transform: translateY(-1px);
}
/* The suggested amount, marked the way the pricing page marks a recommended
   plan — a border, not a fill, so it does not outrank the $3 tile as a target. */
.contribute-option.is-featured { border-color: var(--gold-deep); background: rgba(var(--theme-lit-rgb), 0.5); }

.contribute-note {
  margin: 0;
  color: var(--silver-dim);
  font-size: 0.76rem;
  line-height: 1.45;
  text-align: center;
}

@media (max-width: 30em) {
  .contribute-options { grid-template-columns: minmax(0, 1fr); }
}

/* Older encounter dialogs predate the shared modal markup but use the same
   close glyph. Normalize their box and centering without changing behavior. */
/* Password reveal. The selector carries body.app-body so it outranks the
   blanket gold-gradient button rule, which is (0,1,2) and !important. */
.password-field { position: relative; display: block; }
.password-field > input { width: 100%; padding-right: 2.7rem; }
body.app-body .password-reveal {
  position: absolute !important;
  right: 0.3rem;
  top: 50%;
  transform: translateY(-50%);
  display: grid !important;
  place-items: center;
  width: 2.1rem !important;
  min-width: 2.1rem !important;
  height: 2.1rem;
  min-height: 2.1rem !important;
  padding: 0 !important;
  border: 0 !important;
  background: transparent !important;
  box-shadow: none !important;
  color: var(--silver-edge) !important;
  cursor: pointer;
  transition: color 0.15s;
}
body.app-body .password-reveal:hover,
body.app-body .password-reveal:focus-visible { color: var(--gold) !important; }
body.app-body .password-reveal svg { width: 19px; height: 19px; }
body.app-body .password-reveal .eye-slash { display: none; }
body.app-body .password-reveal.revealed { color: var(--gold) !important; }
body.app-body .password-reveal.revealed .eye-slash { display: block; }

/* AppShell.notify — the guild's own confirmation, in place of window.alert. */
.guild-toast {
  position: fixed;
  left: 50%;
  bottom: 2.2rem;
  transform: translateX(-50%);
  z-index: 9500;
  max-width: min(32rem, calc(100vw - 2rem));
  padding: 0.75rem 1.1rem;
  border: 1px solid var(--gold);
  border-radius: var(--radius-sm, 5px);
  background: var(--maroon-deep);
  color: var(--silver);
  box-shadow: 0 12px 34px rgba(0, 0, 0, 0.55), 0 0 18px var(--gold-glow);
  font-size: 0.9rem;
  line-height: 1.4;
  text-align: center;
  cursor: pointer;
  animation: guild-toast-in 0.22s ease-out;
}
.guild-toast.problem { border-color: #ff9a9a; color: #ffd9d9; box-shadow: 0 12px 34px rgba(0, 0, 0, 0.55); }
.guild-toast.leaving { opacity: 0; transition: opacity 0.2s; }
@keyframes guild-toast-in {
  from { opacity: 0; transform: translate(-50%, 0.6rem); }
  to   { opacity: 1; transform: translate(-50%, 0); }
}
@media (prefers-reduced-motion: reduce) { .guild-toast { animation: none; } }

/* The glyph is drawn rather than typed. Centring a "×" character never
   quite works: U+00D7 sits on the font's mathematical axis, which is above
   the optical centre of the em box, so a perfectly centred line box still
   renders a cross that floats high. Two rotated bars are centred by
   construction, inherit the button's color, and look the same in every
   font on every platform. */
.encounters-modal-close {
  display: inline-grid !important;
  width: 34px !important;
  height: 34px !important;
  min-width: 34px !important;
  min-height: 34px !important;
  place-items: center !important;
  padding: 0 !important;
  line-height: 1 !important;
}

.project-delete-confirm-content { width: min(28rem, 100%); }
.project-delete-confirm-body {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.55rem;
  padding: clamp(1.25rem, 4vw, 2rem);
  text-align: center;
}
.project-delete-confirm-body p { margin: 0; line-height: 1.5; }
.project-delete-confirm-body input { width: min(18rem, 100%); margin-top: 0.45rem; text-align: center; }
.project-delete-confirm-actions { display: flex; flex-wrap: wrap; justify-content: center; gap: 0.75rem; }
.project-delete-confirm-actions button { min-width: 7rem; }

/* Final shared treatment for every × close control, including the two legacy
   encounter/room overlays whose page-local CSS loads after this stylesheet. */
/* #room-tooltip-close is listed separately: an id inside :is() lends the WHOLE
   selector id-level specificity (1,1,1), which silently outranked every
   class-based override of these four buttons. */
body.app-body :is(.settings-modal-close, .encounters-modal-close, .upgrade-access-close, .modal-close, .early-access-banner-close),
body.app-body #lexicon-tooltip-close,
body.app-body #room-tooltip-close {
  display: inline-grid !important;
  width: var(--modal-close-size, 34px) !important;
  height: var(--modal-close-size, 34px) !important;
  min-width: var(--modal-close-size, 34px) !important;
  min-height: var(--modal-close-size, 34px) !important;
  place-items: center !important;
  padding: 0 !important;
  color: var(--silver-dim) !important;
  background: transparent !important;
  border: 1px solid var(--maroon-edge) !important;
  border-radius: var(--radius-sm) !important;
  font-family: Arial, sans-serif !important;
  font-size: 1.3rem !important;
  font-weight: 400 !important;
  line-height: 1 !important;
  text-align: center !important;
  cursor: pointer !important;
}
body.app-body :is(.settings-modal-close, .encounters-modal-close, .upgrade-access-close, .modal-close, .early-access-banner-close):hover,
body.app-body #lexicon-tooltip-close:hover,
body.app-body #room-tooltip-close:hover {
  color: var(--gold) !important;
  border-color: var(--gold) !important;
  background: transparent !important;
}

/* The × is drawn rather than typed. Centring a "×" character never quite
   works: U+00D7 sits on the font's mathematical axis, which is above the
   optical centre of the em box, so even a perfectly centred line box renders a
   cross that floats high. Two rotated bars are centred by construction,
   inherit the button's color, and look identical in every font. This has to
   sit after the shared rule above and match its specificity, because that rule
   forces a font-size with !important.

   The two tooltip ids are listed apart from the :is() for the same reason the
   shared rule above lists them apart: an id inside :is() lends the WHOLE
   selector id-level specificity, and this block's `position` would then
   outrank the corner rule further down — which is what left every dialog's ×
   sitting in the header's flex flow instead of pinned to the corner. The
   bars need a positioned button to be centred in; which kind of positioning
   is the corner rule's business, so `relative` is only the default here. */
body.app-body :is(.settings-modal-close, .encounters-modal-close, .upgrade-access-close, .modal-close, .early-access-banner-close),
body.app-body #room-tooltip-close,
body.app-body #lexicon-tooltip-close {
  position: relative !important;
  font-size: 0 !important;          /* hides the literal × in the markup */
}
body.app-body :is(.settings-modal-close, .encounters-modal-close, .upgrade-access-close, .modal-close, .early-access-banner-close, #room-tooltip-close, #lexicon-tooltip-close)::before,
body.app-body :is(.settings-modal-close, .encounters-modal-close, .upgrade-access-close, .modal-close, .early-access-banner-close, #room-tooltip-close, #lexicon-tooltip-close)::after {
  content: "";
  position: absolute;
  top: 50%;
  left: 50%;
  width: calc(var(--modal-close-size, 34px) * .44);
  height: 1.5px;
  border-radius: 1px;
  background: currentColor;
}
body.app-body :is(.settings-modal-close, .encounters-modal-close, .upgrade-access-close, .modal-close, .early-access-banner-close, #room-tooltip-close, #lexicon-tooltip-close)::before { transform: translate(-50%, -50%) rotate(45deg); }
body.app-body :is(.settings-modal-close, .encounters-modal-close, .upgrade-access-close, .modal-close, .early-access-banner-close, #room-tooltip-close, #lexicon-tooltip-close)::after  { transform: translate(-50%, -50%) rotate(-45deg); }

body.app-body .upgrade-access-dialog > .modal-close {
  position: absolute !important;
  top: var(--modal-close-inset, .65rem);
  right: var(--modal-close-inset, .65rem);
}

/* Every dialog close control occupies the same upper-right position and uses
   the same drawn-X treatment. Dialogs choose an appropriate control size via
   inherited custom properties so compact confirmations do not get an
   oversized button and full generator dialogs retain a comfortable target. */
.settings-modal-content,
.encounters-modal-content,
.upgrade-access-dialog,
.home-preview-dialog {
  position: relative;
  --modal-close-size: 34px;
  --modal-close-inset: .75rem;
}

/* Full-size generator settings. */
.settings-modal-content:not(.save-modal-content):not(.locale-modal-content):not(.locale-delete-modal-content):not(.campaign-delete-content):not(.project-delete-confirm-content) {
  --modal-close-size: 36px;
}

/* Compact confirmations, save dialogs, and access prompts. A dialog that only
   asks one short question takes the small control: the two delete
   confirmations and the two one-question prompts sat in different buckets and
   so wore different-sized ×s beside the same kind of question. */
:is(.save-modal-content, .locale-delete-modal-content, .campaign-delete-content, .project-delete-confirm-content, .upgrade-access-dialog,
    .character-delete-modal-content, .d20-roll-modal-content, .background-ability-modal-content) {
  /* Marked, like the medium bucket below, so membership of a bucket is what
     picks the size rather than how long the full-size rule's :not() chain is. */
  --modal-close-size: 30px !important;
  --modal-close-inset: .6rem;
}

/* Medium VTT and transfer dialogs. */
:is(.locale-modal-content, .encounter-edit-modal-content, .encounter-generator-settings-content, .transfer-adventure-dialog, .encounters-modal-content) {
  --modal-close-size: 34px !important;
}

.settings-modal-content > .settings-modal-head {
  padding-right: calc(var(--modal-close-size) + var(--modal-close-inset) * 2);
}

/* Keep the close control out of the header's layout flow. This also covers
   image previews and custom dialog headers, whose title can otherwise wrap
   and leave the close button stranded on a second row. */
body.app-body :is(.settings-modal-content, .encounters-modal-content) :is(.settings-modal-close, .encounters-modal-close),
body.app-body :is(.upgrade-access-dialog, .home-preview-dialog) > .modal-close {
  position: absolute !important;
  top: var(--modal-close-inset, .75rem) !important;
  right: var(--modal-close-inset, .75rem) !important;
  z-index: 2;
}

.encounters-modal-content > .encounters-modal-header {
  padding-right: calc(var(--modal-close-size) + var(--modal-close-inset));
}

.settings-tabs {
  flex: 0 0 auto;
  display: flex;
  gap: 0.3rem;
  padding: 0.6rem 1.25rem 0;
  background: rgba(var(--theme-deep-rgb), 0.65);
}
.settings-tab {
  background: transparent;
  border: 1px solid transparent;
  border-bottom: none;
  border-radius: var(--radius-sm) var(--radius-sm) 0 0;
  color: var(--silver-dim);
  padding: 0.5rem 1rem;
  font-size: 0.9rem;
  letter-spacing: 0.05em;
  cursor: pointer;
  white-space: nowrap;
  transition: color 0.15s, background 0.15s;
}
.settings-tab:hover { color: var(--gold-bright); }
.settings-tab[aria-selected="true"] {
  color: var(--gold);
  font-weight: 700;
  background: rgba(var(--theme-lit-rgb), 0.65);
  border-color: var(--gold-deep);
}

.settings-panes {
  flex: 1 1 auto;
  min-height: 0;
  padding: 1.25rem;
  display: flex;
}

/* Only the selected pane is in the layout at all. */
.settings-modal .settings-section {
  display: none;
  flex: 1 1 auto;
  min-height: 0;
  max-height: 100%;
  flex-direction: column;
  gap: 1rem;
  overflow-y: auto;
  overscroll-behavior: contain;
}
.settings-modal .settings-section.active { display: flex; }

.settings-hint {
  font-size: 0.85rem;
  color: var(--silver-dim);
  margin: 0;
  letter-spacing: 0.03em;
}

.settings-grid {
  display: grid;
  /* Two columns for as long as they fit; phones drop to one and let the pane
     scroll — see the 520px block at the end of this section. */
  grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
  gap: 0.9rem 1.1rem;
  align-content: start;
}
/* A grid item's default `min-width: auto` is the select's widest option text,
   which on a narrow column is wider than the column itself — the select then
   overflows and the dialog's `overflow: hidden` slices it. Letting the field
   shrink hands the truncation back to the select, which ellipsizes properly. */
.settings-grid .field { min-width: 0; }
/* In a pane the grid keeps its natural height; only the slider bank grows,
   otherwise the fields squash and their contents spill. */
.settings-section > .settings-grid { flex: 0 0 auto; }
.settings-grid .field .hint,
.settings-modal .field .hint {
  text-transform: none; letter-spacing: 0;
  color: var(--silver-dim); font-size: 0.78rem;
}
.settings-grid .field.inline {
  flex-direction: row; align-items: center; justify-content: space-between;
  border: 1px solid var(--maroon-edge); border-radius: var(--radius-sm);
  padding: 0.55rem 0.7rem; background: var(--maroon-deep);
}
.settings-grid .field input[type="range"] { width: 100%; accent-color: var(--gold); }
.settings-grid .field input[type="checkbox"] { width: 18px; height: 18px; accent-color: var(--gold); }
.readout { color: var(--gold); font-weight: 700; margin-left: 0.25rem; }

/* ── Vertical percentage sliders ─────────────────────────────────────
   Coloring is left to accent-color so the platform renders the standards-
   based vertical track and thumb consistently. */
.encounters-sliders {
  flex: 1 1 auto;
  min-height: 0;
  display: flex;
  justify-content: center;
  align-items: stretch;
  gap: clamp(0.4rem, 2vw, 2rem);
  padding: 0.25rem 0;
}
.encounter-slider-group {
  display: flex; flex-direction: column; align-items: center;
  gap: 0.5rem; flex: 1 1 0; min-width: 0; min-height: 0;
}
.encounter-slider-group label {
  flex: 0 0 auto;
  font-size: 0.78rem; text-transform: uppercase;
  color: var(--silver-dim); letter-spacing: 0.05em;
  text-align: center; line-height: 1.2; word-break: break-word;
}
.encounter-slider-group input[type="range"] {
  flex: 1 1 auto;
  min-height: 90px;
  width: 42px;
  writing-mode: vertical-lr;
  direction: rtl;
  accent-color: var(--gold);
  background: var(--maroon-deep);
  border: 1px solid var(--maroon-edge);
  border-radius: 4px;
  cursor: pointer;
}
.encounter-value {
  flex: 0 0 auto;
  font-size: 0.88rem; color: var(--silver); font-weight: 600; text-align: center;
}
.encounters-total {
  flex: 0 0 auto; text-align: center;
  font-size: 0.92rem; color: var(--silver-dim);
}
.encounters-total span { color: var(--gold); font-weight: 700; }

.settings-modal-footer {
  flex: 0 0 auto;
  display: flex; justify-content: flex-end; gap: 0.75rem;
  padding: 0.85rem 1.25rem;
  border-top: 1px solid var(--maroon-edge);
}
.settings-modal-footer button {
  background: var(--gold);
  color: var(--on-accent);
  border: none;
  padding: 0.5rem 1.2rem;
  font-size: 0.88rem; font-weight: 700;
  border-radius: var(--radius-sm);
  cursor: pointer;
  text-transform: uppercase; letter-spacing: 0.06em;
  transition: background 0.15s;
}
.settings-modal-footer button:hover { background: var(--gold-bright); }

.settings-modal-footer.project-delete-confirm-actions {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 0.75rem;
  justify-content: stretch;
}
.settings-modal-footer.project-delete-confirm-actions button {
  width: 100%;
  min-width: 0;
  margin: 0;
  white-space: nowrap;
}

@media (max-width: 64em), (max-height: 47.5em) {
  /* A wider margin on touch screens: the point of the gap is to give a thumb
     somewhere to land to dismiss the dialog. */
  .settings-modal { padding: 6vh 6vw; }
  .settings-modal-content { width: min(620px, 100%); }
  .settings-tabs { padding: 0.5rem 0.75rem 0; overflow-x: auto; }
  .settings-tab { padding: 0.45rem 0.7rem; font-size: 0.84rem; }
  .settings-panes { padding: 0.9rem 0.75rem; }
  .settings-modal-head, .settings-modal-footer { padding: 0.7rem 0.75rem; }
}
/* Phones: the dialog stacks. Sideways is the wrong axis on a narrow screen —
   a horizontal tab strip pushes its last tab off the edge where nothing hints
   it exists, and two columns of fields leave each select narrower than the
   words in it. Both go vertical, and the pane scrolls to pay for the height. */
@media (max-width: 520px) {
  .settings-tabs {
    flex-direction: column;
    gap: 0.25rem;
    padding: 0.6rem 0.75rem;
    overflow-x: visible;                    /* undoes the compact strip */
  }
  /* Stacked, a tab is a full-width row rather than a folder tab standing on
     the pane below it, so it gets all four corners and a border to match. */
  .settings-tab {
    width: 100%;
    text-align: left;
    border: 1px solid transparent;
    border-radius: var(--radius-sm);
  }
  .settings-tab[aria-selected="true"] { border-color: var(--gold-deep); }

  .settings-grid { grid-template-columns: 1fr; gap: 0.7rem; }

  /* One column is taller than a phone, so the pane takes on the scrolling the
     dialog itself will not do. */
  .settings-modal .settings-section.active {
    overflow-y: auto;
    overscroll-behavior: contain;
  }

  .encounter-slider-group label { font-size: 0.68rem; letter-spacing: 0; }
  .encounter-slider-group input[type="range"] { width: 34px; }
  .encounter-value { font-size: 0.8rem; }
}

/* ══════════════════════════════════════════════════════════════════════
   5c. Map viewer — fit, zoom and pan, shared by every SVG map page
   ══════════════════════════════════════════════════════════════════════
   #map-container is a fixed viewport onto a much larger drawing; #map is the
   movable stage and zoom/pan are one transform on it. A colossal dungeon is
   fitted rather than clipped, and only the on-screen view is transformed —
   export and print use the original markup. */

body.app-body .map-view {
  flex: 1 1 auto;
  min-height: 0;
  height: auto;
  position: relative;
  overflow: hidden;
  display: block;
  background: rgba(var(--theme-abyss-rgb), 0.42);
  padding: 0;
  border-radius: var(--radius);
  border: 1px solid var(--gold-deep);
  box-shadow: inset 0 1px 0 var(--gold-glow), 0 10px 44px rgba(0, 0, 0, 0.7);
  touch-action: none;                    /* we drive panning ourselves */
}
body.app-body .map-view.pannable { cursor: grab; }
body.app-body .map-view.panning  { cursor: grabbing; }

/* Consistent first-run state for every map generator. The card is real
   markup (rather than generated content) so its instructions and settings
   action remain accessible and generator-specific. */
body.app-body .map-placeholder {
  position: absolute;
  top: 50%;
  left: 50%;
  z-index: 2;
  transform: translate(-50%, -50%);
  width: min(70%, 420px);
  aspect-ratio: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 0.75rem;
  padding: 1.5rem;
  color: var(--silver-dim);
  font-size: 0.9rem;
  text-align: center;
  background: linear-gradient(180deg, var(--maroon-lit) 0%, var(--maroon) 100%);
  border: 2px dashed var(--maroon-edge);
  border-radius: 4px;
}
body.app-body .map-placeholder.hidden { display: none; }
body.app-body .map-placeholder .placeholder-icon { opacity: 0.45; }
body.app-body .map-placeholder .placeholder-lead {
  color: var(--silver);
  font-family: var(--font-display);
  font-size: 1.15rem;
  letter-spacing: 0.06em;
}
body.app-body .map-placeholder .placeholder-hint {
  max-width: 30ch;
  line-height: 1.5;
}
body.app-body .map-placeholder .placeholder-hint strong {
  color: var(--gold);
  font-weight: 600;
}
body.app-body .map-placeholder .placeholder-action {
  margin-top: 0.15rem;
  padding: 0.4rem 0.9rem !important;
  color: var(--gold) !important;
  font-size: 0.82rem !important;
  background: transparent !important;
  border: 1px solid var(--gold-deep) !important;
  box-shadow: none !important;
}
body.app-body .map-placeholder .placeholder-action:hover:not(:disabled) {
  background: rgba(var(--theme-accent-rgb), 0.12) !important;
}
/* While the opening map is being drawn there is nothing to press, so the
   placeholder shows only its icon and the "drawing" line. */
body.app-body .map-placeholder.is-drawing .placeholder-hint,
body.app-body .map-placeholder.is-drawing .placeholder-action { display: none; }

.map-view .map-stage {
  position: absolute;
  top: 0; left: 0;
  transform-origin: 0 0;
  will-change: transform;
}
.map-view .map-stage svg,
.map-view .map-stage canvas { display: block; }

.map-controls {
  position: absolute;
  top: 0.6rem; right: 0.6rem;
  z-index: 5;
  display: none;                          /* shown once a map exists */
  align-items: center;
  gap: 0.3rem;
  padding: 0.3rem;
  background: rgba(var(--theme-abyss-rgb), 0.82);
  border: 1px solid var(--gold-deep);
  border-radius: 999px;
  box-shadow: 0 4px 18px rgba(0, 0, 0, 0.5);
}
.map-controls.shown { display: flex; }
.map-controls button {
  background: transparent;
  color: var(--gold);
  border: 1px solid transparent;
  border-radius: 999px;
  min-width: 30px; height: 30px;
  padding: 0 0.55rem;
  font-size: 1rem; font-weight: 700; line-height: 1;
  cursor: pointer;
  transition: background 0.15s, border-color 0.15s, color 0.15s;
}
.map-controls button:hover:not(:disabled) {
  background: rgba(var(--theme-accent-rgb), 0.18);
  border-color: var(--gold);
}
.map-controls button:disabled { opacity: 0.35; cursor: not-allowed; }
.map-controls .zoom-reset { font-size: 0.78rem; letter-spacing: 0.06em; text-transform: uppercase; }
.map-controls .zoom-level {
  min-width: 3.4rem; text-align: center;
  font-size: 0.78rem; color: var(--silver);
  font-variant-numeric: tabular-nums; user-select: none;
}

/* ══════════════════════════════════════════════════════════════════════
   6. Tables and forms (account / admin screens)
   ══════════════════════════════════════════════════════════════════════ */

.guild-table { width: 100%; border-collapse: collapse; font-size: 0.92rem; }
.guild-table th {
  text-align: left; padding: 0.55rem 0.7rem;
  font-size: 0.8rem; text-transform: uppercase; letter-spacing: 0.08em;
  color: var(--gold); background: var(--maroon-deep);
}
.guild-table td { padding: 0.55rem 0.7rem; border-top: 1px solid var(--maroon-edge); }
.guild-table tr:hover td { background: rgba(var(--theme-accent-rgb), 0.05); }

.form-card { width: min(430px, 100%); display: flex; flex-direction: column; gap: 1rem; }
.form-note { font-size: 0.88rem; color: var(--silver-dim); line-height: 1.5; }
.form-error {
  color: var(--danger); font-size: 0.9rem;
  border-left: 3px solid var(--danger); padding-left: 0.7rem;
}
.form-ok {
  color: var(--gold); font-size: 0.9rem;
  border-left: 3px solid var(--gold); padding-left: 0.7rem;
}
a.link { color: var(--gold); text-decoration: none; }
a.link:hover { text-decoration: underline; }

.centre-stage {
  flex: 1 1 auto; min-height: 0;
  display: flex; align-items: center; justify-content: center;
}

/* ══════════════════════════════════════════════════════════════════════
   7. Page transition — the fantasy wipe between rooms of the guild
   ══════════════════════════════════════════════════════════════════════
   A gold sigil expands out of darkness on arrival and collapses back into
   it on departure, so moving between generators reads as one continuous
   app rather than seven separate documents. */

#pt-veil {
  position: fixed; inset: 0; z-index: 9999;
  pointer-events: none; opacity: 0;
  background:
    radial-gradient(circle at 50% 50%, rgba(var(--theme-accent-rgb), 0.16) 0%, transparent 38%),
    radial-gradient(circle at 50% 50%, rgba(var(--theme-lit-rgb), 0.96) 30%, rgba(var(--theme-abyss-rgb), 1) 72%);
}
/* The fifth swatch is each palette's contrast color, and it already answers
   the rule this needs: in every dark palette it is the light one (Gold,
   Silver, Tan, Orange, Lilac, White), and in both light palettes it is the
   dark one (Ink, Black). So the sigil reads against the veil either way
   without the stylesheet having to ask which kind of theme is on. */
#pt-veil .pt-sigil {
  position: absolute; top: 50%; left: 50%;
  width: 260px; height: 260px; margin: -130px 0 0 -130px;
  opacity: 0;
  color: var(--theme-swatch-5, var(--gold));
}
body.pt-enter #pt-veil { opacity: 1; pointer-events: all; animation: pt-open 0.62s ease-out forwards; }
body.pt-enter #pt-veil .pt-sigil { animation: pt-sigil-open 0.62s ease-out forwards; }
body.pt-leave #pt-veil { opacity: 0; pointer-events: all; animation: pt-close 0.42s ease-in forwards; }
body.pt-leave #pt-veil .pt-sigil { animation: pt-sigil-close 0.42s ease-in forwards; }

@keyframes pt-open  { from { opacity: 1; clip-path: circle(150% at 50% 50%); }
                      to   { opacity: 0; clip-path: circle(0%   at 50% 50%); } }
@keyframes pt-close { from { opacity: 0; clip-path: circle(0%   at 50% 50%); }
                      to   { opacity: 1; clip-path: circle(150% at 50% 50%); } }
@keyframes pt-sigil-open  { 0% { opacity: 0.9; transform: rotate(0deg)    scale(1.6); }
                            100% { opacity: 0; transform: rotate(-120deg) scale(0.2); } }
@keyframes pt-sigil-close { 0% { opacity: 0;   transform: rotate(-120deg) scale(0.2); }
                            60% { opacity: 1;  transform: rotate(-20deg)  scale(1.1); }
                            100% { opacity: 0.9; transform: rotate(0deg)  scale(1.6); } }

/* Content settles in behind the veil.

   The `.settling` class is added at boot and removed by app-shell.js the
   moment the animation ends — it must NOT stay on the element. A running or
   filling animation makes its element a containing block for `position:
   fixed` descendants, which silently re-anchors and clips every overlay a
   page puts inside its content area (modals, tooltips) to the content box
   instead of the viewport. Scoping the animation to a class that is taken
   off again keeps the effect and drops the side effect. */
body.app-body.settling > .app-main,
body.app-body.settling > .app,
body.app-body.settling > main { animation: content-settle 0.5s ease-out both; }
@keyframes content-settle {
  from { opacity: 0; transform: translateY(10px) scale(0.994); }
  to   { opacity: 1; transform: none; }
}

@media (prefers-reduced-motion: reduce) {
  #pt-veil, #pt-veil .pt-sigil { animation: none !important; opacity: 0 !important; }
  body.app-body.settling > .app-main,
  body.app-body.settling > .app,
  body.app-body.settling > main { animation: none; }
  .nav-link { transition: none; }
}

/* ══════════════════════════════════════════════════════════════════════
   8. Tablet and phone — drawer, focused workspace, floating legend
   ══════════════════════════════════════════════════════════════════════
   In constrained logical viewports the shell rearranges itself around one idea: the map (or the
   text output) is the whole screen, and everything else is summoned over it
   and dismissed again.

     · the side menu folds into a hamburger and slides in as a drawer,
       taking the nav links and the identity buttons with it
     · the map key / building legend leaves the sidebar and becomes a panel
       floating over the map, opened from its own button
     · Generate floats back out of the drawer as a button of its own, so the
       one control you press repeatedly is not behind the menu
     · the working area gets the full width and still never scrolls the page

   app-shell.js does the reparenting on the same breakpoint and puts it all
   back on the way up, so COMPACT_QUERY there must match the query below. */

.nav-burger, #legend-fab, #generate-fab, #controls-fab,
#app-drawer, #app-controls, #app-scrim, #app-legend { display: none; }

#compact-actions {
  position: fixed; left: 0.85rem; bottom: 0.85rem; z-index: 936;
  display: flex; align-items: center; gap: 0.6rem;
}
#compact-actions #generate-fab,
#compact-actions #controls-fab { position: static; }

/* ── The hamburger ─────────────────────────────────────────────────── */
.nav-burger {
  align-items: center; justify-content: center;
  width: 42px; height: 38px; flex: 0 0 auto;
  background: transparent; border: 1px solid var(--maroon-edge);
  border-radius: var(--radius-sm); color: var(--gold);
  cursor: pointer; padding: 0;
  transition: border-color 0.15s, background 0.15s;
}
.nav-burger:hover, .nav-burger[aria-expanded="true"] {
  border-color: var(--gold); background: rgba(var(--theme-accent-rgb), 0.12);
}
.nav-burger .bars { display: block; width: 20px; height: 14px; position: relative; }
.nav-burger .bars::before, .nav-burger .bars::after, .nav-burger .bars span {
  content: ""; position: absolute; left: 0; right: 0; height: 2px;
  background: currentColor; border-radius: 2px;
  transition: transform 0.25s, opacity 0.2s;
}
.nav-burger .bars::before { top: 0; }
.nav-burger .bars span    { top: 6px; }
.nav-burger .bars::after  { top: 12px; }
.nav-burger[aria-expanded="true"] .bars::before { transform: translateY(6px) rotate(45deg); }
.nav-burger[aria-expanded="true"] .bars span    { opacity: 0; }
.nav-burger[aria-expanded="true"] .bars::after  { transform: translateY(-6px) rotate(-45deg); }

/* ── The drawer ────────────────────────────────────────────────────── */
#app-scrim {
  position: fixed; inset: 0; z-index: 940;
  background: rgba(var(--theme-abyss-rgb), 0.62);
  opacity: 0; pointer-events: none;
  transition: opacity 0.28s;
}
body.drawer-open #app-scrim,
body.controls-open #app-scrim { opacity: 1; pointer-events: auto; }

#app-drawer {
  position: fixed; left: 0; top: var(--nav-h); bottom: 0; z-index: 945;
  width: min(340px, 86vw);
  flex-direction: column;
  background: var(--maroon-deep);
  box-shadow: 10px 0 40px rgba(0, 0, 0, 0.65);
  transform: translateX(-102%);
  transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
body.drawer-open #app-drawer { transform: none; }

#app-controls {
  position: fixed; right: 0; top: var(--nav-h); bottom: 0; z-index: 946;
  width: min(340px, 90vw);
  flex-direction: column;
  background: var(--maroon-deep);
  box-shadow: -10px 0 40px rgba(0, 0, 0, 0.65);
  transform: translateX(102%);
  transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
body.controls-open #app-controls { transform: none; }
#app-controls .controls-head {
  flex: 0 0 auto; display: flex; align-items: center; justify-content: space-between;
  padding: 0.65rem var(--panel-pad); border-bottom: 1px solid var(--maroon-edge);
  color: var(--gold); font-family: var(--font-display); letter-spacing: 0.1em;
}
#app-controls .controls-body {
  flex: 1 1 auto; min-height: 0; overflow-y: auto;
  padding: var(--panel-pad); display: flex; flex-direction: column; gap: var(--gap);
}
#app-controls .sidebar { width: 100% !important; overflow: visible; flex: 0 0 auto; }

#app-drawer .drawer-body {
  flex: 1 1 auto; min-height: 0;
  overflow-y: auto; overscroll-behavior: contain;
  padding: var(--panel-pad);
  display: flex; flex-direction: column; gap: var(--gap);
}
.drawer-label {
  font-family: var(--font-display); font-size: 0.82rem;
  font-variant: small-caps; letter-spacing: 0.14em;
  color: var(--gold-deep); margin-bottom: -0.45rem;
}

/* Nav links stack once they live in the drawer. */
#app-drawer .nav-links {
  display: flex; flex-direction: column; align-items: stretch;
  gap: 0.15rem; list-style: none;
}
#app-drawer .nav-link {
  display: block; padding: 0.6rem 0.8rem; font-size: 1rem;
  transform: none;                       /* no scaling in a vertical list */
}
#app-drawer .nav-link.active { transform: none; font-weight: 700; }

/* In the drawer the groups form an accordion tree. The current page's group
   starts open, and selecting a different heading reveals its sister pages. */
#app-drawer .nav-group { display: block; }
#app-drawer .nav-group + .nav-group { margin-top: 0.25rem; }

#app-drawer .nav-group-btn {
  width: 100%;
  padding: 0 0 0.35rem 0;
  font-size: 0.82rem;
  letter-spacing: 0.14em;
  color: var(--gold-deep);
  cursor: pointer;
  border-bottom: 1px solid var(--maroon-edge);
  border-radius: 0;
  /* These links act as accordion headings on their first activation. */
  text-align: left;
}
#app-drawer .nav-group-btn:hover { background: rgba(var(--theme-accent-rgb), 0.09); color: var(--gold); }
/* Gold text is enough to say "you are in here" — a filled heading would
   compete with the highlighted row that marks the actual page. */
#app-drawer .nav-group-btn.active { color: var(--gold); background: transparent; }
#app-drawer .nav-caret { display: inline-block; }

#app-drawer .nav-submenu {
  display: none;
  position: static;
  min-width: 0;
  padding: 0.3rem 0 0 0;
  gap: 0.15rem;
  background: transparent;
  border: none;
  box-shadow: none;
}
#app-drawer .nav-group.open > .nav-submenu { display: flex; }
#app-drawer .nav-submenu .nav-link { padding: 0.55rem 0.8rem; }

/* The page control stack, filling the drawer width. */
#app-drawer .sidebar {
  width: 100%; overflow: visible;        /* the drawer body does the scrolling */
  flex: 0 0 auto;
}

/* Sits at the top of the drawer, so it is separated from what follows
   rather than from what came before. */
#app-drawer .nav-aside {
  margin-left: 0; flex-wrap: wrap; gap: 0.6rem; align-items: center;
  padding-bottom: 0.9rem; border-bottom: 1px solid var(--maroon-edge);
  font-size: 0.92rem;
}
#app-drawer .nav-aside a, #app-drawer .nav-aside button { padding: 0.4rem 0.85rem; }

/* In the drawer there is nothing to drop down into — the drawer is already
   the menu. The name becomes a heading and the items sit open beneath it. */
#app-drawer .user-menu { width: 100%; display: block; }
#app-drawer .nav-aside .user-menu .who {
  width: 100%;
  color: var(--gold);
  border: none;
  background: transparent;
  padding: 0 0 0.4rem 0;
  max-width: none;
  cursor: default;
  justify-content: flex-start;
}
#app-drawer .who-caret { display: none; }
#app-drawer .user-menu-list {
  display: flex;
  position: static;
  min-width: 0;
  padding: 0;
  background: none;
  border: none;
  box-shadow: none;
  gap: 0.35rem;
}
#app-drawer .nav-aside .user-menu-list a {
  border: 1px solid var(--maroon-edge);
  padding: 0.4rem 0.85rem;
}
#app-drawer .nav-aside .user-menu-list a.sign-out { margin-top: 0.25rem; border-radius: var(--radius-sm); }

/* ── The floating legend ───────────────────────────────────────────── */
#legend-fab {
  position: fixed; right: 0.85rem; bottom: 0.85rem; z-index: 936;
  align-items: center; gap: 0.4rem;
  padding: 0.6rem 1rem; border-radius: 999px;
  background: linear-gradient(180deg, var(--gold-bright) 0%, var(--gold) 55%, var(--gold-deep) 100%);
  color: var(--on-accent); border: 1px solid var(--gold-deep);
  font-size: 0.9rem; font-weight: 700; letter-spacing: 0.06em; text-transform: uppercase;
  cursor: pointer; box-shadow: 0 6px 22px rgba(0, 0, 0, 0.55), 0 0 18px var(--gold-glow);
}
#legend-fab:active { transform: translateY(1px); }

/* ── The floating generate button ──────────────────────────────────── */
/* The legend button's twin in the opposite corner. In compact layout the
   sidebar — and with it the page's real Generate button — lives inside the
   drawer, so redrawing a map would otherwise mean opening the menu, pressing
   Generate and waiting for the drawer to slide away, every single time. This
   presses the real button; the page's own wiring is untouched. */
#generate-fab {
  position: fixed; left: 0.85rem; bottom: 0.85rem; z-index: 936;
  align-items: center; gap: 0.4rem;
  padding: 0.6rem 1rem; border-radius: 999px;
  background: linear-gradient(180deg, var(--gold-bright) 0%, var(--gold) 55%, var(--gold-deep) 100%);
  color: var(--on-accent); border: 1px solid var(--gold-deep);
  font-size: 0.9rem; font-weight: 700; letter-spacing: 0.06em; text-transform: uppercase;
  cursor: pointer; box-shadow: 0 6px 22px rgba(0, 0, 0, 0.55), 0 0 18px var(--gold-glow);
}
#generate-fab:active { transform: translateY(1px); }
/* Mirrors the real button, which pages disable while a map is drawing. */
#generate-fab:disabled { opacity: 0.45; cursor: not-allowed; box-shadow: none; }

#controls-fab {
  width: 42px;
  height: 42px;
  min-height: 42px;
  z-index: 936;
  align-items: center;
  justify-content: center;
  padding: 0;
  border: 1px solid var(--gold-deep);
  border-radius: var(--radius-sm);
  background: var(--maroon-deep);
  color: var(--gold);
  font-family: var(--font-display);
  font-size: 0.76rem;
  font-variant: small-caps;
  letter-spacing: 0.1em;
  cursor: pointer;
  box-shadow: 0 5px 18px rgba(0, 0, 0, 0.45);
}
#controls-fab svg {
  width: 20px; height: 20px; fill: none; stroke: currentColor;
  stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round;
}
#controls-fab:hover,
#controls-fab[aria-expanded="true"] { background: var(--maroon-lit); border-color: var(--gold); }

body:not(.no-controls-tab) #legend-fab { bottom: calc(0.85rem + 42px + 0.6rem); }
body:not(.no-controls-tab) #app-legend { bottom: calc(0.85rem + 42px + 0.6rem + 46px + 0.6rem); }

#app-legend {
  position: fixed; right: 0.85rem; z-index: 935;
  bottom: calc(0.85rem + 46px + 0.6rem);       /* clear of its own button */
  width: min(310px, 86vw);
  max-height: min(62vh, 30rem);
  flex-direction: column;
  background: linear-gradient(180deg, var(--maroon-lit) 0%, var(--maroon) 100%);
  border: 1px solid var(--gold-deep); border-radius: var(--radius);
  box-shadow: 0 16px 46px rgba(0, 0, 0, 0.7), 0 0 30px rgba(var(--theme-accent-rgb), 0.12);
  transform: translateY(14px) scale(0.97);
  opacity: 0; pointer-events: none;
  transition: transform 0.24s cubic-bezier(0.2, 0.7, 0.3, 1), opacity 0.24s;
}
body.legend-open #app-legend { transform: none; opacity: 1; pointer-events: auto; }

#app-legend .legend-head {
  flex: 0 0 auto; display: flex; align-items: center; justify-content: space-between;
  gap: 0.75rem; padding: 0.6rem 0.85rem; background: var(--maroon-deep);
  border-radius: var(--radius) var(--radius) 0 0;
}
#app-legend .legend-head strong {
  font-family: var(--font-display); font-variant: small-caps;
  letter-spacing: 0.12em; font-size: 0.95rem; color: var(--gold); font-weight: 600;
}
#app-legend .legend-body {
  flex: 1 1 auto; min-height: 0;
  overflow-y: auto; overscroll-behavior: contain;
  padding: 0.75rem 0.85rem;
  display: flex; flex-direction: column; gap: 1rem;
}
/* Never let a panel be squeezed: without this the two legends a page can
   carry shrink past their content and draw on top of each other. */
#app-legend .legend-body > * { flex: 0 0 auto; }

/* Panels lose their own frame in here — the floating panel is the frame. */
#app-legend #map-key,
#app-legend #building-legend {
  width: 100%; background: none; border: none; box-shadow: none;
  padding: 0; min-height: 0; height: auto; overflow: visible;
}
/* Two legends on one page (the city has both) get a rule between them. */
#app-legend .legend-body > * + * {
  padding-top: 1rem; border-top: 1px solid var(--maroon-edge);
}
#app-legend #map-key h3,
#app-legend #building-legend h3 {
  text-align: left; margin: 0 0 0.6rem 0; padding: 0;
}
#app-legend #building-legend.wide { width: 100%; }

/* ══ The compact layout ════════════════════════════════════════════ */
/* The height clause is not optional: app-shell.js collapses the bar on either
   axis, so a wide-but-short window (a 1366x768 laptop, any window shortened on
   a desktop) had its links moved into the drawer by the script while this
   block stayed inert — no hamburger, no drawer, and the whole of the
   navigation simply gone. Both breakpoints must state the same condition. */
@media (max-width: 64em), (max-height: 47.5em) {
  :root { --pad-page: 0.85rem; --gap: 0.85rem; --panel-pad: 0.95rem; }
  /* Was 16px, which shrank every rem-based size on exactly the screens that
     could least afford it — the spell designer ended up with 65 separate runs
     of text under 12px on a phone. Density on a small screen has to come from
     scrolling, not from type nobody can read. */
  html { font-size: 17px; }

  /* The gutter still applies, so the hamburger lines up with the workspace
     below it. Only the vertical padding tightens. */
  .app-nav { padding: 0.5rem var(--page-gutter); gap: 0.6rem; }

  /* On a phone specifically, lift the root again so the smallest label in the
     app (0.65rem) lands on 12px rather than 10.4px. Raising the root moves
     every size together and leaves the design's proportions untouched. */
  @media (max-width: 34em) {
    html { font-size: 18.5px; }
    /* Below this, app-shell.js moves #nav-aside into the drawer itself; this
       is the no-JS/pre-hydration fallback so it never doubles up in the bar. */
    .app-nav > .nav-aside { display: none; }
  }
  .app-nav .nav-brand {
    margin-right: 0; font-size: 0.95rem; letter-spacing: 0.08em;
    overflow: hidden; text-overflow: ellipsis;
  }
  .nav-burger { display: inline-flex; }
  #app-drawer, #app-controls, #app-scrim { display: flex; }

  /* Anything still sitting in the bar is chrome the drawer now owns. Identity
     is different: with the links gone its own margin-left:auto already pins
     it to the top-right corner of the bar, so on a tablet-width compact
     screen it simply stays put — only below the phone breakpoint does the
     drawer claim it too (see the nested query and app-shell.js). */
  .app-nav > .nav-links { display: none; }

  /* The workspace is the screen: one column, full width, no side rail. */
  body.app-body > .app-main,
  body.app-body > .app,
  body.app-body > main { flex-direction: column; }

  .map-area, #map-section, .main, .sheet-area {
    width: 100%; flex: 1 1 auto; min-width: 0; min-height: 0;
  }

  /* Only the toggled overlay shows a legend; in-flow copies stand down.
     These carry a body.app-body prefix because the pages style the same IDs
     and their <style> block loads after this file — equal specificity would
     lose on source order. */
  body.app-body #building-legend,
  body.app-body #map-key { display: none; }
  #app-legend #map-key, #app-legend #building-legend { display: block; }
  #app-legend, #legend-fab { display: flex; }
  body.no-legend #app-legend, body.no-legend #legend-fab { display: none; }

  /* Only on pages whose Generate button the drawer swallowed — see
     app-shell.js, which sets no-generate everywhere else. */
  #generate-fab { display: flex; }
  body.no-generate #generate-fab { display: none; }
  #controls-fab { display: flex; }
  body.no-controls-tab #controls-fab { display: none; }
  /* Give the map every pixel it can have, and let the parchment frame hug it
     instead of stretching — a square map on a tall phone otherwise floats in
     a screenful of empty cream.

     `:not(.map-view)` is load-bearing. A .map-view frame is a fixed viewport
     with an absolutely positioned stage inside it, so `height: auto` would
     collapse it to nothing; those frames must keep filling the column. */
  body.app-body #map-section,
  body.app-body .map-area { justify-content: center; }
  body.app-body #map-container:not(.map-view) {
    padding: 8px;
    flex: 0 1 auto; height: auto; max-height: 100%;
  }
  body.app-body #map-container.map-view {
    flex: 1 1 auto; height: auto; max-height: none;
  }
  .options { gap: 0.7rem; }
}

@media (max-width: 35em) {
  :root { --pad-page: 0.6rem; }
  .app-nav .nav-brand { font-size: 0.85rem; }
  #legend-fab, #generate-fab { padding: 0.55rem 0.85rem; font-size: 0.82rem; }
  /* Stacked controls read better than squeezed columns on a phone. The
     body.app-body prefix is load-bearing: the pages style .options too, and
     their <style> block wins any tie on source order. */
  body.app-body .options { flex-direction: column; align-items: stretch; }
  body.app-body .options .field select,
  body.app-body .options .field input[type="number"] { min-width: 0; width: 100%; }
  body.app-body .options #generate-btn,
  body.app-body #generate-btn { width: 100%; }
}

/* Fullscreen is entered on the first interaction on compact screens. Keep a
   touch-friendly escape hatch above page modals and device safe areas. */
.compact-fullscreen-toggle,
.compact-fullscreen-message {
  display: none;
}

@media (max-width: 64em), (max-height: 47.5em) {
  .compact-fullscreen-toggle {
    position: fixed;
    right: max(0.65rem, env(safe-area-inset-right));
    bottom: max(0.65rem, env(safe-area-inset-bottom));
    z-index: 10050;
    display: block;
    min-height: 2.75rem;
    padding: 0.55rem 0.8rem;
    border: 1px solid var(--gold);
    border-radius: 999px;
    color: var(--gold-bright);
    background: rgba(var(--theme-abyss-rgb), 0.92);
    box-shadow: 0 0.2rem 0.8rem rgba(0, 0, 0, 0.45);
    font: 700 0.72rem/1 var(--font-body);
    letter-spacing: 0.04em;
    cursor: pointer;
  }

  .compact-fullscreen-message {
    position: fixed;
    right: max(0.65rem, env(safe-area-inset-right));
    bottom: calc(max(0.65rem, env(safe-area-inset-bottom)) + 3.4rem);
    z-index: 10049;
    display: block;
    max-width: min(22rem, calc(100vw - 1.3rem));
    padding: 0.7rem 0.85rem;
    border: 1px solid var(--maroon-edge);
    border-radius: 0.45rem;
    color: var(--silver);
    background: rgba(var(--theme-abyss-rgb), 0.96);
    opacity: 0;
    pointer-events: none;
    transform: translateY(0.4rem);
    transition: opacity 160ms ease, transform 160ms ease;
  }

  .compact-fullscreen-message.show {
    opacity: 1;
    transform: none;
  }
}

@media (prefers-reduced-motion: reduce) {
  #app-drawer, #app-scrim, #app-legend { transition: none; }
}

/* ======================================================================
   Shared presentation contract
   Page-local CSS owns diagrams and generated-content geometry; this block
   owns the visual language that must remain consistent between modules.
   ====================================================================== */
body.app-body {
  font-size: var(--body-size);
  line-height: 1.45;
}

body.app-body :is(.dice-page > h1, .cc-page-title, .lib-head h1) {
  color: var(--silver) !important;
  font-family: var(--font-display) !important;
  font-size: clamp(1.35rem, 2.2vw, 2rem) !important;
  letter-spacing: 0.08em;
  line-height: 1.2;
}

/* #map-key and #building-legend are addressed separately below for the same
   reason as the heading rule: an id inside :is() lifts the whole selector to
   id-strength and makes it unoverridable by any class. */
body.app-body :is(.panel, .settings-bar,
  .output, .sheet, .settings-modal-content, .generator-controls-column),
body.app-body #map-key,
body.app-body #building-legend {
  color: var(--silver) !important;
  background: rgba(var(--theme-base-rgb), var(--panel-opacity)) !important;
  border-color: var(--maroon-edge) !important;
  border-radius: var(--radius) !important;
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.07),
    0 6px 22px rgba(0, 0, 0, 0.42) !important;
  backdrop-filter: blur(5px);
}

/* The two id selectors that used to sit in this :is() list have been split
   out below. Inside :is(), the whole selector takes the specificity of its
   most specific argument — so `#map-key h3` was quietly giving this rule
   id-strength (1,1,2), and no class-based rule could override it. That is
   what defeated the parchment heading color. */
body.app-body :is(h2, .panel-title, .settings-bar h3) {
  color: var(--gold) !important;
  font-family: var(--font-display) !important;
  font-size: var(--section-heading-size) !important;
  line-height: 1.25;
}
body.app-body :is(#map-key, #building-legend) h3 {
  color: var(--gold) !important;
  font-family: var(--font-display) !important;
  font-size: var(--section-heading-size) !important;
  line-height: 1.25;
}

body.app-body :is(h3, h4, h5, h6) {
  color: var(--gold) !important;
  font-family: var(--font-display) !important;
  font-size: var(--subheading-size) !important;
  line-height: 1.3;
}

/* …except on parchment. The two rules above paint every heading in the app
   gold, which is correct on maroon and unreadable on cream — it is what put
   the pricing plan names and the roadmap column headings at 1.4:1. These are
   the only light surfaces in the app, so they are the only exception. */
body.app-body :is(.price-card, .roadmap-board article, .paper-surface)
  :is(h1, h2, h3, h4, h5, h6, .panel-title) {
  color: var(--paper-gold) !important;
}

/* A panel that is deliberately parchment rather than maroon. The rule above
   forces every .panel to the maroon ground with !important, which silently
   defeated the magic-item description column: it asked for #eee5d0, rendered
   at rgba(56,16,26,.6), and kept painting its text in parchment inks — which
   is why an item's own name sat at 1.39:1. Opting in with .paper-surface
   makes the intent explicit and survives that rule. */
body.app-body .paper-surface {
  background: var(--paper) !important;
  color: var(--paper-ink) !important;
  border-color: var(--paper-line) !important;
}

/* Designer help ---------------------------------------------------------
   A compact, consistent entry point and instruction layout shared by the
   monster, spell, class, and magic-item workbenches. */
.designer-heading-with-help {
  display: flex; align-items: center; justify-content: space-between; gap: 0.75rem;
}
.designer-help-button {
  flex: 0 0 auto; display: inline-grid; place-items: center;
  width: 1.55rem; height: 1.55rem; border: 1px solid var(--gold);
  border-radius: 50%; color: var(--gold); background: transparent;
  font-family: Georgia, serif; font-size: 0.92rem; font-weight: 800;
  font-style: italic; line-height: 1; text-decoration: none; cursor: pointer;
}
.designer-help-button:hover { color: var(--gold-bright); border-color: var(--gold-bright); }
.designer-instructions-modal { width: min(48rem, 100%); }
.designer-instructions-body { min-width: 0; overflow-y: auto; padding: 1rem 1.15rem 1.2rem; }
.designer-instructions-intro {
  margin: 0 0 1rem; color: var(--silver); font-size: 0.94rem; line-height: 1.55;
}
.designer-instruction-steps {
  margin: 0; padding: 0; list-style: none; counter-reset: instruction-step;
  display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 0.7rem;
}
.designer-instruction-steps li {
  counter-increment: instruction-step; position: relative; min-width: 0;
  padding: 0.75rem 0.8rem 0.75rem 2.75rem;
  border: 1px solid var(--maroon-edge); border-radius: 7px;
  background: rgba(var(--theme-abyss-rgb), 0.48);
}
.designer-instruction-steps li::before {
  content: counter(instruction-step); position: absolute; left: 0.75rem; top: 0.72rem;
  display: grid; place-items: center; width: 1.35rem; height: 1.35rem;
  border: 1px solid var(--gold); border-radius: 50%; color: var(--gold);
  font-size: 0.72rem; font-weight: 800;
}
.designer-instruction-steps strong { display: block; margin-bottom: 0.25rem; color: var(--gold-bright); font-size: 0.86rem; }
.designer-instruction-steps span { display: block; color: var(--silver-dim); font-size: 0.8rem; line-height: 1.45; }
@media (max-width: 650px) {
  .designer-instruction-steps { grid-template-columns: minmax(0, 1fr); }
  .designer-instructions-modal { max-height: calc(100dvh - 2rem); display: flex; flex-direction: column; }
  .designer-instructions-body { flex: 1 1 auto; min-height: 0; }
}
/* Panels nested inside a parchment surface are parchment too — otherwise the
   .panel rule fills them with maroon and they read as dusty mauve cards
   floating on cream. */
body.app-body .paper-surface :is(.panel, .output) {
  background: rgba(255, 255, 255, 0.42) !important;
  border-color: var(--paper-line) !important;
  color: var(--paper-ink) !important;
  box-shadow: inset 0 0 18px rgba(92, 57, 11, 0.06) !important;
}

/* A parchment surface also shadows the app's text tokens for its subtree, so
   descendants inherit parchment inks instead of the dark-ground silvers. */
body.app-body .paper-surface {
  --silver: var(--paper-ink);
  --silver-dim: var(--paper-muted);
  --silver-edge: var(--paper-muted);
  --gold: var(--paper-gold);
  --gold-bright: var(--paper-gold);
}

body.app-body :is(.field > label, legend, dt) {
  color: var(--gold) !important;
  font-size: 0.9rem;
  font-weight: 700;
}

body.app-body :is(input, select, textarea) {
  min-height: 2.35rem;
  color: var(--silver) !important;
  background: rgba(var(--theme-abyss-rgb), 0.72) !important;
  border-color: var(--maroon-edge) !important;
}

/* The generic control background above intentionally normalizes page-specific
   fields, but its shorthand also clears background images. Reapply the shared
   dropdown affordance here, after that reset, so every native selector keeps
   its chevron and distinct trigger area regardless of a page's local CSS. */
body.app-body select:not([multiple]) {
  padding-right: 3rem !important;
  background-image:
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='9' viewBox='0 0 14 9'%3E%3Cpath d='m2 2 5 5 5-5' fill='none' stroke='%23e6c04a' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"),
    linear-gradient(to left, rgba(var(--theme-accent-rgb), 0.14) 0 2.45rem, transparent 2.45rem) !important;
  background-repeat: no-repeat !important;
  background-position: right 0.75rem center, right center !important;
  background-size: 14px 9px, 100% 100% !important;
  cursor: pointer;
}

body.app-body select:not([multiple]):hover:not(:disabled) {
  border-color: var(--gold-deep) !important;
}

body.app-body select:disabled {
  cursor: not-allowed;
  opacity: 0.55;
}

:root[data-theme-mode="light"] body.app-body select:not([multiple]) {
  background-image:
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='9' viewBox='0 0 14 9'%3E%3Cpath d='m2 2 5 5 5-5' fill='none' stroke='%235c390b' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"),
    linear-gradient(to left, rgba(var(--theme-accent-rgb), 0.14) 0 2.45rem, transparent 2.45rem) !important;
}

body.app-body :is(.sidebar, .generator-controls-column) {
  width: var(--sidebar-width) !important;
  flex: 0 0 var(--sidebar-width) !important;
  min-height: 0;
  overflow-y: auto;
  overscroll-behavior: contain;
}

body.app-body :is(.panel, .settings-bar, .output, .sheet) {
  padding: var(--panel-pad);
}

body.app-body :is(.main, .map-area, .sheet-area, .npc-main, .dice-center) {
  min-width: 0;
  min-height: 0;
}

/* Map-generator status belongs to the drawing surface instead of occupying a
   row beneath it. This keeps the content panel level with its side columns. */
body.app-body:is([data-page="dungeon"], [data-page="cavern"], [data-page="castle"], [data-page="city"]) :is(#map-section, .map-area) {
  position: relative;
}

body.app-body:is([data-page="dungeon"], [data-page="cavern"], [data-page="castle"], [data-page="city"]) :is(#map-section, .map-area) > #status {
  position: absolute;
  left: 50%;
  bottom: 0.8rem;
  z-index: 8;
  width: max-content;
  max-width: calc(100% - 2rem);
  min-height: 0;
  margin: 0;
  padding: 0.4rem 0.7rem;
  transform: translateX(-50%);
  border-radius: var(--radius-sm);
  background: rgba(var(--theme-abyss-rgb), 0.82);
  text-align: center;
  pointer-events: none;
}

body.app-body:is([data-page="dungeon"], [data-page="cavern"], [data-page="castle"], [data-page="city"]) :is(#map-section, .map-area) > #status:empty {
  display: none;
}

body.app-body:is([data-page="dungeon"], [data-page="cavern"], [data-page="castle"], [data-page="city"]) :is(#map-section, .map-area) > #status.loading {
  top: 50%;
  bottom: auto;
  transform: translate(-50%, -50%);
  color: var(--gold);
  animation: pulse 1s ease-in-out infinite;
}

body.app-body :is(.scroll-pane, [role="tabpanel"], .panel[style*="overflow"],
  .sidebar, .generator-controls-column, .settings-panes) {
  overscroll-behavior: contain;
}

/* Fey Magic: green-yellow remains exclusive to major page/result titles;
   cyan supplies readable body copy and the smaller hierarchy. */
html[data-color-theme="purple-lilac"] body.app-body {
  color: #00e5ff;
}
html[data-color-theme="purple-lilac"] body.app-body :is(.panel, .settings-bar,
  #map-key, #building-legend, .output, .sheet, .settings-modal-content,
  .generator-controls-column) {
  color: #00e5ff !important;
  background: rgba(var(--theme-deep-rgb), var(--panel-opacity)) !important;
}
html[data-color-theme="purple-lilac"] body.app-body :is(h2, h3, h4, h5, h6,
  .panel-title, .field > label, legend, dt) {
  color: #00e5ff !important;
}
html[data-color-theme="purple-lilac"] body.app-body :is(h1, .npc-title h2, .kingdom-name) {
  color: #b6ff5c !important;
}

/* The second half of the old Celestia override layer stood here. It is gone
   for the same reason as the first: the palette now carries its own values. */

/* Page titles are colored where they are defined (see h1.module-title and
   the .dice-page/.cc-page-title rule) — through --silver, so they follow the
   palette instead of the near-white they used to hardcode. */

@media (max-width: 64em), (max-height: 47.5em) {
  body.app-body:is(
    [data-page="dungeon"], [data-page="cavern"], [data-page="castle"],
    [data-page="city"], [data-page="kingdom"], [data-page="text"],
    [data-page="treasure"], [data-page="weather"], [data-page="puzzles"],
    [data-page="hazards-traps"], [data-page="encounter"]
  ) :is(h1.module-title, .module-intro) {
    margin-left: var(--page-gutter);
    margin-right: var(--page-gutter);
  }

  body.app-body :is(.sidebar, .generator-controls-column) {
    width: 100% !important;
    flex: 0 0 auto !important;
    overflow: visible;
  }
  body.app-body > :is(.app-main, .app, main) {
    padding: clamp(0.5rem, 1.5vw, 0.85rem) var(--page-gutter);
    gap: clamp(0.5rem, 1.5vw, 0.85rem);
  }
}

@media (max-width: 35em) {
  :root {
    --panel-pad: clamp(0.55rem, 2.5vw, 0.8rem);
    --section-heading-size: 1rem;
    --subheading-size: 0.95rem;
  }
  html body.app-body h1.module-title {
    margin-top: 0.45rem;
    font-size: clamp(1.2rem, 6vw, 1.55rem);
  }
}
/* Subscription-locked destinations remain visible so members can discover them. */
.access-locked {
  opacity: .58;
  cursor: not-allowed !important;
}
button.map-action[aria-disabled="true"],
.save-option[aria-disabled="true"] {
  opacity: .58 !important;
  cursor: not-allowed !important;
  filter: saturate(.55);
}

.upgrade-access-shade {
  position: fixed;
  inset: 0;
  z-index: 100000;
  display: grid;
  place-items: center;
  padding: 1rem;
  background: rgba(0, 0, 0, .58);
}

.upgrade-access-dialog {
  position: relative;
  width: min(28rem, 100%);
  padding: 2.75rem 2rem 2rem;
  border: 1px solid var(--gold, #d7ad43);
  border-radius: 1rem;
  background: var(--panel, #2b0d14);
  color: var(--text, #f5ead7);
  text-align: center;
  box-shadow: 0 1rem 3rem rgba(0, 0, 0, .45);
}

.upgrade-access-dialog h2 { margin: 0 0 .65rem; }
.upgrade-access-copy { margin: 0 auto 1.35rem; max-width: 23rem; color: var(--silver, #d8cdbc); line-height: 1.5; }
.upgrade-access-pricing { display: inline-flex; align-items: center; justify-content: center; }
.upgrade-access-actions { display: flex; flex-wrap: wrap; justify-content: center; gap: .75rem; }
.upgrade-access-close {
  position: absolute;
  top: .65rem;
  right: .65rem;
}
/* Prevent the default palette from reaching the screen while the deferred
   shell restores the saved palette. applyColorTheme adds this attribute
   synchronously as soon as app-shell.js executes. */
html:not([data-color-theme]) { visibility: hidden; }
.transfer-adventure-dialog{width:min(34rem,calc(100vw - 2rem));max-height:calc(100vh - 2rem);overflow:hidden;display:flex;flex-direction:column}.transfer-adventure-dialog form{display:flex;flex:1 1 auto;min-height:0;flex-direction:column;gap:.7rem}.transfer-adventure-choices{display:grid;gap:.7rem;min-height:0;overflow-y:auto;padding-right:.25rem}.transfer-adventure-option{display:flex;align-items:center;gap:.75rem;padding:.75rem;border:1px solid color-mix(in srgb,var(--gold) 45%,transparent);border-radius:.5rem;cursor:pointer}.transfer-adventure-option:has(input:checked){border-color:var(--gold);background:color-mix(in srgb,var(--gold) 12%,transparent)}.transfer-adventure-option input[type="radio"]{width:auto}.transfer-adventure-option span{display:grid;flex:1 1 auto;gap:.15rem;min-width:0}.transfer-adventure-option small{color:var(--silver-dim)}#new-adventure-name{box-sizing:border-box;width:100%}.transfer-adventure-dialog .form-error,.transfer-adventure-dialog button[type="submit"]{flex:0 0 auto}
.save-generated-map-dialog form{display:grid;gap:.85rem;text-align:left}

/* Generator save dialogs (dungeon, cavern, …). Shared so every Save looks
   the same; it used to live privately inside the dungeon page. */
.generator-about-content { width: min(760px, 100%); }
.generator-about-body {
  min-height: 0;
  overflow-y: auto;
  padding: 1rem 1.25rem 1.25rem;
  color: var(--silver);
  line-height: 1.55;
}
.generator-about-body :is(p, ol, dl) { margin: 0 0 1rem; }
.generator-about-body h3 { margin: 1rem 0 .45rem; color: var(--gold-bright); }
.generator-about-body ol { padding-left: 1.3rem; }
.generator-about-body li + li { margin-top: .4rem; }
.generator-about-body dt { margin-top: .65rem; color: var(--gold); font-weight: 800; }
.generator-about-body dd { margin: .15rem 0 0; color: var(--silver-dim); }
.generator-about-body a { color: var(--gold-bright); }

/* Full-viewport page contracts. These selectors intentionally outrank the
   older page-local one-line bundles that otherwise cap or de-flex the Guild
   and tabletop workspaces. */
body.app-body > main.vtt-dashboard {
  box-sizing: border-box;
  width: 100% !important;
  min-width: 0;
  min-height: 0;
  margin: 0 !important;
  padding: clamp(1.25rem, 4vw, 3rem) var(--page-gutter) 3rem !important;
  align-content: start;
  overflow-y: auto;
}
body.app-body > main.vtt-layout {
  flex: 1 1 auto !important;
  width: 100%;
  min-width: 0;
  min-height: 0;
}
body.app-body > main.hosted-chat-page {
  box-sizing: border-box;
  width: min(52rem, calc(100% - var(--page-gutter) * 2)) !important;
  min-width: 0;
  min-height: 0 !important;
  margin: 0 auto !important;
  padding: 1rem !important;
  overflow: hidden;
}
body.app-body[data-section="players"] > main.token-app {
  grid-template-columns: minmax(23rem, .75fr) minmax(0, 1.25fr) !important;
}
@media (max-width: 850px) {
  body.app-body[data-section="players"] > main.token-app {
    grid-template-columns: minmax(0, 1fr) !important;
  }
}

.save-modal-content {
    width: min(30rem, calc(100vw - 2rem));
}

.save-name-field {
    display: grid;
    gap: 0.35rem;
    margin: 1.25rem 1.25rem 1rem;
}

.save-name-field .form-error:empty {
    display: none;
}

.save-name-field .form-error {
    margin: 0;
    font-size: 0.8rem;
}

.save-name-field input.invalid {
    border-color: #ff9a9a;
}

.save-options {
    display: grid;
    gap: 0.8rem;
    margin: 0 1.25rem 1.25rem;
}

.save-option {
    display: grid;
    gap: 0.35rem;
    padding: 0.9rem 1rem;
    background: transparent;
    color: var(--silver);
    border: 1px solid var(--maroon-edge);
    border-radius: var(--radius-sm, 5px);
    cursor: pointer;
    text-align: left;
    transition: border-color 0.15s, background 0.15s;
}

.save-option strong {
    color: inherit;
    font-size: inherit;
    font-weight: inherit;
    letter-spacing: inherit;
}

.save-option span {
    font-size: 0.8rem;
    line-height: 1.4;
    color: var(--silver-dim);
}

.save-option:hover, .save-option:focus-visible {
    border-color: var(--gold);
    background: rgba(230, 192, 74, 0.08);
}
