/* -----------------------------------------------------------------------
   Cork D&D – Global Styles
   Supplements compiled Tailwind. Reserved for plain CSS that doesn't need
   Tailwind processing (parchment-mode overrides, scrollbar, EasyMDE chrome).
   Tailwind @apply utilities live in tailwind.input.css.
   ----------------------------------------------------------------------- */

/* ================================================================
   iOS SAFE AREAS (F-165)
   viewport-fit=cover + the black-translucent status bar let the
   installed app paint under the iPhone notch and home indicator.
   The fixed chrome (top bar, sidebar, mobile tab bar, full-screen
   chat surfaces) pads itself by these tokens so its controls stay
   tappable. Custom properties rather than raw env() at every site
   so browser tests can simulate a notch by overriding --safe-top.
   ================================================================ */
:root {
  --safe-top: env(safe-area-inset-top, 0px);
  --safe-bottom: env(safe-area-inset-bottom, 0px);
}

/* ================================================================
   COLOR SCHEME → NATIVE UI SKIN
   iOS/Android pick the on-screen KEYBOARD's light/dark skin (plus
   autofill panels and UA form-control chrome) from the page's
   declared color-scheme — undeclared, the dark tavern got a glaring
   white keyboard. Follows the same html.dark toggle as the theme.
   ================================================================ */
html { color-scheme: dark; }
html:not(.dark) { color-scheme: light; }

/* ================================================================
   NO FOCUS-ZOOM ON PHONES
   iOS Safari auto-zooms the whole page when a focused text control's
   font-size is under 16px — and never zooms back out on dismiss, so
   the layout stayed cropped/shifted after typing (the chat composer,
   at text-sm/14px, hit this on every message). Minimum 16px for text
   controls below the desktop breakpoint kills the zoom at the root.
   ================================================================ */
@media (max-width: 1023px) {
  input[type="text"], input[type="search"], input[type="email"],
  input[type="password"], input[type="number"], input[type="url"],
  input[type="tel"], textarea, select {
    font-size: 16px !important;
  }
}

/* Custom scrollbar for a more immersive feel */
::-webkit-scrollbar { width: 6px; }
::-webkit-scrollbar-track { background: #1c1917; /* stone-900 */ }
::-webkit-scrollbar-thumb { background: #57534e; border-radius: 10px; /* stone-600 */ }

/* The profile tab row (members/profile.html) scrolls sideways on a phone by
   touch alone: no track in either theme (not a colour, so no parchment remap).
   Without this the custom scrollbar above painted a stub track on desktop. */
.profile-tabs { scrollbar-width: none; }
.profile-tabs::-webkit-scrollbar { display: none; }
/* The table workspace tab row (tables/table_detail.html, F-218): the same
   touch-only sideways scroller on a phone. */
.workspace-tabs { scrollbar-width: none; }
.workspace-tabs::-webkit-scrollbar { display: none; }
/* The Wiki tab's two panes (tables/_tab_wiki.html, F-218 phase 2) are one
   screen at a time under lg: the tree, or — once a page is open
   (data-wiki-open, flipped by a tree row click / "All pages" in
   table_detail.html) — the page. From lg both show side by side. Display
   only, so it is theme-neutral. */
@media (max-width: 1023.98px) {
  .wiki-split[data-wiki-open] .wiki-tree-col { display: none; }
  .wiki-split:not([data-wiki-open]) .wiki-pane-col { display: none; }
}

/* Smooth page transitions — for ANCHOR navigation.
   This applies to PROGRAMMATIC scrolls too: `scrollTop = 0` and
   `window.scrollTo(x, y)` on the document silently animate over ~300ms. Any
   scroll that must snap has to pass `behavior: "instant"` (#281/#284). */
@media (prefers-reduced-motion: no-preference) {
  html { scroll-behavior: smooth; }
}

/* ================================================================
   PARCHMENT LIGHT MODE
   Remaps dark Tailwind utility classes when html.dark is absent.
   No templates need modification — all overrides live here.
   ================================================================ */

html:not(.dark) body { background-color: #f5e6c8; color: #3d2b1f; }

/* --- Backgrounds --- */
html:not(.dark) .bg-stone-950     { background-color: #fdf6e3 !important; }
html:not(.dark) .bg-stone-900     { background-color: #f5e6c8 !important; }
html:not(.dark) .bg-stone-800     { background-color: #e8d5a3 !important; }
html:not(.dark) .bg-stone-700     { background-color: #d4b896 !important; }
html:not(.dark) .bg-stone-800\/95 { background-color: rgba(232,213,163,0.97) !important; }
html:not(.dark) .bg-stone-800\/80 { background-color: rgba(232,213,163,0.92) !important; }
html:not(.dark) .bg-stone-800\/50 { background-color: rgba(232,213,163,0.5) !important; }
html:not(.dark) .bg-stone-800\/40 { background-color: rgba(232,213,163,0.4) !important; }
html:not(.dark) .bg-stone-900\/80 { background-color: rgba(245,230,200,0.92) !important; }
html:not(.dark) .bg-stone-900\/50 { background-color: rgba(245,230,200,0.5) !important; }
html:not(.dark) .bg-stone-900\/40 { background-color: rgba(245,230,200,0.4) !important; }
html:not(.dark) .bg-stone-700\/80 { background-color: rgba(212,184,150,0.8) !important; }
html:not(.dark) .bg-stone-700\/60 { background-color: rgba(212,184,150,0.6) !important; }
html:not(.dark) .bg-stone-700\/50 { background-color: rgba(212,184,150,0.5) !important; }
html:not(.dark) .bg-stone-700\/20 { background-color: rgba(212,184,150,0.2) !important; }

/* --- Card & Utility Overrides --- */
html:not(.dark) .tavern-card { background-color: rgba(232,213,163,0.5) !important; border-color: rgba(196,168,130,0.6) !important; }
html:not(.dark) .tavern-card:hover { border-color: rgba(180,83,9,0.4) !important; box-shadow: 0 10px 15px -3px rgba(180,83,9,0.1) !important; }
html:not(.dark) .golden-border { border-color: rgba(180,83,9,0.3) !important; }
/* Colour ONLY — never a width, a weight or a padding. The baseline box for both
   themes lives in tailwind.input.css; a theme-conditional `border:` shorthand
   silently re-declares the width too and the two themes stop measuring the same
   (tests/test_theme_parity.py). */
html:not(.dark) .btn-primary {
    background-image: linear-gradient(to bottom right, #92400e, #78350f) !important;
    color: #fdf6e3 !important;
    border-color: #451a03 !important;
    box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06) !important;
}
html:not(.dark) .btn-primary:hover {
    background-image: linear-gradient(to bottom right, #b45309, #92400e) !important;
}

/* Parchment draws the secondary button as an OUTLINE where dark draws it filled.
   The heavier edge that reads well on cream is painted, not built: the border
   stays the baseline 1px and an inset ring sits on top of it, so the button
   measures the same in both themes. Done with `border: 2px` instead, every
   secondary button on the site was 2px taller and 2px wider in light — enough,
   on the table page, to squeeze the header's title column and wrap its meta
   line. The second shadow is the baseline's `shadow-sm`: a bare `box-shadow`
   here would delete it. Font weight is NOT overridden; dark's 700 is the
   baseline and a heavier weight is wider, which is layout, not colour. */
html:not(.dark) .btn-secondary {
    background-color: transparent !important;
    color: #7c2d12 !important;
    border-color: #7c2d12 !important;
    box-shadow:
        inset 0 0 0 1px #7c2d12,
        0 1px 2px 0 rgba(0, 0, 0, 0.05) !important;
}
html:not(.dark) .btn-secondary:hover {
    background-color: #7c2d12 !important;
    color: #fdf6e3 !important;
}

/* --- Text --- */
html:not(.dark) .text-white      { color: #2c1810 !important; }
html:not(.dark) .text-stone-100  { color: #3d2b1f !important; }
html:not(.dark) .text-stone-200  { color: #3d2b1f !important; }
html:not(.dark) .text-stone-300  { color: #5c3d2e !important; }
html:not(.dark) .text-stone-400  { color: #7a5c4a !important; }
html:not(.dark) .text-stone-500  { color: #9b7b6a !important; }
html:not(.dark) .text-stone-600  { color: #b89880 !important; }
html:not(.dark) .text-amber-50   { color: #3d2b1f !important; }
html:not(.dark) .text-amber-100  { color: #3d2b1f !important; }
html:not(.dark) .text-amber-400  { color: #7c2d12 !important; }
html:not(.dark) .text-amber-500  { color: #b45309 !important; }

/* --- Borders --- */
html:not(.dark) .border-stone-800       { border-color: #d4b896 !important; }
html:not(.dark) .border-stone-700       { border-color: #c4a882 !important; }
html:not(.dark) .border-stone-700\/60   { border-color: rgba(196,168,130,0.6) !important; }
html:not(.dark) .border-stone-700\/50   { border-color: rgba(196,168,130,0.5) !important; }
html:not(.dark) .border-stone-700\/40   { border-color: rgba(196,168,130,0.4) !important; }
html:not(.dark) .border-stone-600\/50   { border-color: rgba(168,148,118,0.5) !important; }
html:not(.dark) .border-amber-600       { border-color: #d97706 !important; }
html:not(.dark) .border-amber-600\/40   { border-color: rgba(217,119,6,0.4) !important; }
html:not(.dark) .border-amber-900\/20   { border-color: rgba(120,53,15,0.15) !important; }

/* --- Amber background washes --- */
/* Solid amber-500 (the F-138 wiki "new" dots): raw #f59e0b washes out on the
   cream background, so parchment darkens it to amber-700 — the same shift the
   .text-amber-500 remap makes. */
html:not(.dark) .bg-amber-500   { background-color: #b45309 !important; }
html:not(.dark) .bg-amber-900\/60 { background-color: rgba(217,119,6,0.15) !important; }
html:not(.dark) .bg-amber-900\/50 { background-color: rgba(217,119,6,0.12) !important; }
html:not(.dark) .bg-amber-900\/40 { background-color: rgba(217,119,6,0.10) !important; }
html:not(.dark) .bg-amber-950\/40 { background-color: rgba(217,119,6,0.10) !important; }
html:not(.dark) .bg-amber-950\/35 { background-color: rgba(217,119,6,0.10) !important; }
html:not(.dark) .bg-amber-950\/25 { background-color: rgba(217,119,6,0.09) !important; }
html:not(.dark) .bg-amber-950\/20 { background-color: rgba(217,119,6,0.08) !important; }
/* The profile Adventurer card's Continue button (F-160): the same amber wash family. */
html:not(.dark) .bg-amber-700\/20 { background-color: rgba(217,119,6,0.12) !important; }
/* The active settings row (F-215): a light amber wash on parchment — a shade
   stronger than the /20 ghost buttons read at, so the selected row still
   stands out from the card behind it. */
html:not(.dark) .bg-amber-600\/12 { background-color: rgba(180,83,9,0.14) !important; }
/* The profile tab row + History filter pills (F-215, tailwind.input.css): the
   active state is a component class (pagination.js toggles `is-active`), so
   the utility remaps above never reach it — the same values, by hand. */
/* The tab's phone-only left hairline is set via @apply, so it's plain CSS here:
   match the border-stone-700/60 parchment value. Side-specific on purpose. */
html:not(.dark) .profile-tab { border-left-color: rgba(196,168,130,0.6) !important; }
html:not(.dark) .profile-tab.is-active,
html:not(.dark) .profile-tab.is-active:hover { color: #7c2d12 !important; border-bottom-color: #d97706 !important; }
html:not(.dark) .profile-pill.is-active,
html:not(.dark) .profile-pill.is-active:hover { color: #7c2d12 !important; }
/* The table workspace tabs + Sessions filter pills (F-218, tailwind.input.css):
   the idle colours are @applied inside the component, so the utility remaps
   never reach them either — the parchment muted/ink/heading-amber values, by hand. */
html:not(.dark) .workspace-tab { color: #7a5c4a !important; border-left-color: rgba(196,168,130,0.6) !important; }
html:not(.dark) .workspace-tab:hover { color: #7c2d12 !important; }
html:not(.dark) .workspace-tab.is-active,
html:not(.dark) .workspace-tab.is-active:hover { color: #7c2d12 !important; border-bottom-color: #d97706 !important; }
html:not(.dark) .workspace-tab-count { color: #b89880 !important; }
html:not(.dark) .workspace-pill { background-color: rgba(232,213,163,0.6) !important; border-color: rgba(196,168,130,0.6) !important; color: #7a5c4a !important; }
html:not(.dark) .workspace-pill:hover { color: #3d2b1f !important; }
html:not(.dark) .workspace-pill.is-active,
html:not(.dark) .workspace-pill.is-active:hover { background-color: rgba(217,119,6,0.18) !important; border-color: rgba(180,83,9,0.4) !important; color: #7c2d12 !important; }
/* The board's Pending pill (F-221): the red idle state, on parchment. The active
   pair above is two classes to this one's one, so an active Pending pill still
   comes out amber. */
html:not(.dark) .workspace-pill-alert { background-color: rgba(239,68,68,0.09) !important; border-color: rgba(185,28,28,0.35) !important; color: #b91c1c !important; }
html:not(.dark) .workspace-pill-alert:hover { color: #991b1b !important; }
/* The Wiki tab's page tree rows (F-218 phase 2, tailwind.input.css .wiki-row). */
html:not(.dark) .wiki-row { color: #5c4a3d !important; }
html:not(.dark) .wiki-row:hover { color: #7c2d12 !important; background-color: rgba(196,168,130,0.3) !important; }
html:not(.dark) .wiki-row.is-active,
html:not(.dark) .wiki-row.is-active:hover { background-color: rgba(217,119,6,0.14) !important; color: #7c2d12 !important; }
html:not(.dark) .hover\:bg-amber-700\/30:hover { background-color: rgba(217,119,6,0.2) !important; }

/* ================================================================
   PARCHMENT HOVER STATES  (site-wide)
   test_light_mode_contrast.py deliberately ignores variant-prefixed
   classes, so dark hover backgrounds and light-toned hover text fell
   through to their raw dark Tailwind values on the cream page — a
   too-dark highlight (menus, list rows) or invisible hover text.
   These twin the base remaps above, so hover matches its own element
   in parchment. Scoped to :not(.dark) — dark mode is untouched.
   ================================================================ */
/* Dark hover backgrounds → a soft parchment highlight (mirrors bg-stone-700/800). */
html:not(.dark) .hover\:bg-stone-500:hover       { background-color: #c4a882 !important; }
html:not(.dark) .hover\:bg-stone-700:hover       { background-color: #d4b896 !important; }
html:not(.dark) .hover\:bg-stone-700\/20:hover   { background-color: rgba(212,184,150,0.25) !important; }
html:not(.dark) .hover\:bg-stone-700\/30:hover   { background-color: rgba(212,184,150,0.35) !important; }
html:not(.dark) .hover\:bg-stone-700\/40:hover   { background-color: rgba(212,184,150,0.40) !important; }
html:not(.dark) .hover\:bg-stone-700\/50:hover   { background-color: rgba(212,184,150,0.50) !important; }
html:not(.dark) .hover\:bg-stone-700\/60:hover   { background-color: rgba(212,184,150,0.60) !important; }
html:not(.dark) .hover\:bg-stone-700\/70:hover   { background-color: rgba(212,184,150,0.55) !important; }
html:not(.dark) .hover\:bg-stone-800:hover       { background-color: #ddc79b !important; }
html:not(.dark) .hover\:bg-stone-800\/30:hover   { background-color: rgba(232,213,163,0.40) !important; }
html:not(.dark) .hover\:bg-stone-800\/60:hover   { background-color: rgba(232,213,163,0.60) !important; }
html:not(.dark) .hover\:bg-stone-800\/70:hover   { background-color: rgba(232,213,163,0.70) !important; }
html:not(.dark) .hover\:bg-stone-800\/80:hover   { background-color: rgba(232,213,163,0.85) !important; }
/* Amber / red hover washes → the parchment amber & red families. */
html:not(.dark) .hover\:bg-amber-600\/60:hover   { background-color: rgba(180,83,9,0.30) !important; }
html:not(.dark) .hover\:bg-amber-900\/30:hover   { background-color: rgba(217,119,6,0.14) !important; }
html:not(.dark) .hover\:bg-amber-900\/35:hover   { background-color: rgba(217,119,6,0.16) !important; }
html:not(.dark) .hover\:bg-amber-900\/50:hover   { background-color: rgba(217,119,6,0.18) !important; }
html:not(.dark) .hover\:bg-red-900\/20:hover     { background-color: rgba(239,68,68,0.10) !important; }
html:not(.dark) .hover\:bg-red-900\/40:hover     { background-color: rgba(239,68,68,0.14) !important; }
/* Light-toned hover text → dark parchment inks (mirrors the base text remaps). */
html:not(.dark) .hover\:text-white:hover         { color: #2c1810 !important; }
html:not(.dark) .hover\:text-stone-100:hover     { color: #3d2b1f !important; }
html:not(.dark) .hover\:text-stone-200:hover     { color: #3d2b1f !important; }
html:not(.dark) .hover\:text-stone-300:hover     { color: #5c3d2e !important; }
html:not(.dark) .hover\:text-amber-300:hover     { color: #7c2d12 !important; }
html:not(.dark) .hover\:text-amber-400:hover     { color: #7c2d12 !important; }
html:not(.dark) .hover\:text-red-200:hover       { color: #991b1b !important; }
html:not(.dark) .hover\:text-red-300:hover       { color: #991b1b !important; }
html:not(.dark) .hover\:text-red-400:hover       { color: #b91c1c !important; }
html:not(.dark) .hover\:text-emerald-400:hover   { color: #047857 !important; }
/* Hover borders → the parchment amber / stone / red edges. */
html:not(.dark) .hover\:border-amber-400:hover     { border-color: #b45309 !important; }
html:not(.dark) .hover\:border-amber-600:hover     { border-color: #d97706 !important; }
html:not(.dark) .hover\:border-amber-700\/50:hover { border-color: rgba(180,83,9,0.5) !important; }
html:not(.dark) .hover\:border-red-700\/50:hover   { border-color: rgba(185,28,28,0.5) !important; }
html:not(.dark) .hover\:border-stone-500:hover     { border-color: #b89880 !important; }
html:not(.dark) .hover\:border-stone-600:hover     { border-color: #c4a882 !important; }

/* --- Status: scheduled (emerald) --- */
html:not(.dark) .bg-emerald-900\/60     { background-color: rgba(16,185,129,0.15) !important; }
html:not(.dark) .text-emerald-200,
html:not(.dark) .text-emerald-300       { color: #065f46 !important; }
html:not(.dark) .text-emerald-400       { color: #047857 !important; }
html:not(.dark) .text-emerald-500       { color: #065f46 !important; }
html:not(.dark) .border-emerald-800\/35 { border-color: rgba(16,185,129,0.35) !important; }
html:not(.dark) .border-emerald-800\/40 { border-color: rgba(16,185,129,0.4) !important; }

/* --- Status: cancelled / danger (red) --- */
html:not(.dark) .bg-red-900\/60     { background-color: rgba(239,68,68,0.15) !important; }
html:not(.dark) .bg-red-900\/40     { background-color: rgba(239,68,68,0.12) !important; }
html:not(.dark) .bg-red-900\/30     { background-color: rgba(239,68,68,0.10) !important; }
html:not(.dark) .bg-red-900\/20     { background-color: rgba(239,68,68,0.08) !important; }
html:not(.dark) .bg-red-900\/15     { background-color: rgba(239,68,68,0.07) !important; }
html:not(.dark) .text-red-300       { color: #991b1b !important; }
html:not(.dark) .text-red-200       { color: #991b1b !important; }
html:not(.dark) .text-red-400       { color: #b91c1c !important; }
html:not(.dark) .border-red-800\/60 { border-color: rgba(239,68,68,0.35) !important; }
html:not(.dark) .border-red-800\/50 { border-color: rgba(239,68,68,0.30) !important; }
html:not(.dark) .border-red-900\/30 { border-color: rgba(239,68,68,0.20) !important; }

/* --- Flash success (green) --- */
html:not(.dark) .bg-green-900\/40      { background-color: rgba(16,185,129,0.12) !important; }
html:not(.dark) .text-green-300        { color: #065f46 !important; }
html:not(.dark) .border-green-800\/60  { border-color: rgba(16,185,129,0.35) !important; }

/* --- Flash info (blue) --- */
html:not(.dark) .bg-blue-900\/40      { background-color: rgba(59,130,246,0.12) !important; }
html:not(.dark) .bg-blue-900\/15      { background-color: rgba(59,130,246,0.07) !important; }
html:not(.dark) .text-blue-300        { color: #1e3a5f !important; }
html:not(.dark) .text-blue-200        { color: #1e3a5f !important; }
html:not(.dark) .border-blue-800\/60  { border-color: rgba(59,130,246,0.35) !important; }

/* --- Toasts (ajax.js window.ajaxToast — classes live in JS, so the
   light-mode guard test can't see them; keep this block in sync with
   TOAST_STYLES). Toasts float over arbitrary content, so unlike the inline
   flash washes above they need near-opaque light tints (-100 family) to stay
   legible. Text colours reuse the .text-*-200 remaps above. --- */
html:not(.dark) .bg-green-900\/90  { background-color: rgba(220,252,231,0.95) !important; }
html:not(.dark) .bg-red-900\/90    { background-color: rgba(254,226,226,0.95) !important; }
html:not(.dark) .bg-blue-900\/90   { background-color: rgba(219,234,254,0.95) !important; }
html:not(.dark) .bg-amber-900\/90  { background-color: rgba(254,243,199,0.95) !important; }
html:not(.dark) .border-green-800  { border-color: rgba(16,185,129,0.4) !important; }
html:not(.dark) .border-red-800    { border-color: rgba(239,68,68,0.4) !important; }
html:not(.dark) .border-blue-800   { border-color: rgba(59,130,246,0.4) !important; }
html:not(.dark) .border-amber-800  { border-color: rgba(217,119,6,0.4) !important; }

/* --- Game night / session badge (purple) --- */
html:not(.dark) .bg-purple-900\/40     { background-color: rgba(139,92,246,0.12) !important; }
html:not(.dark) .bg-purple-900\/60     { background-color: rgba(139,92,246,0.15) !important; }
html:not(.dark) .text-purple-200,
html:not(.dark) .text-purple-300       { color: #4c1d95 !important; }
html:not(.dark) .text-purple-400       { color: #6d28d9 !important; }
html:not(.dark) .border-purple-900\/40 { border-color: rgba(139,92,246,0.25) !important; }

/* --- Moderator role badge (sky), and the F-224 "shared with named players"
       marker on a wiki page / its prep chip, which borrows the same hue --- */
html:not(.dark) .bg-sky-900\/30     { background-color: rgba(14,165,233,0.10) !important; }
html:not(.dark) .bg-sky-900\/40     { background-color: rgba(14,165,233,0.12) !important; }
html:not(.dark) .text-sky-300       { color: #075985 !important; }
html:not(.dark) .border-sky-800\/40 { border-color: rgba(14,165,233,0.26) !important; }
html:not(.dark) .border-sky-800\/50 { border-color: rgba(14,165,233,0.30) !important; }
html:not(.dark) .hover\:border-sky-500\/60:hover { border-color: rgba(14,165,233,0.45) !important; }

/* --- Scrollbar --- */
html:not(.dark) ::-webkit-scrollbar-track { background: #f5e6c8; }
html:not(.dark) ::-webkit-scrollbar-thumb { background: #c4a882; }

/* --- Browser-native form chrome (datetime pickers, select dropdowns) --- */
html:not(.dark) input,
html:not(.dark) textarea,
html:not(.dark) select { color-scheme: light; }

/* --- FullCalendar light-mode CSS variables --- */
html:not(.dark) .fc {
    --fc-border-color: rgba(196,168,130,0.5);
    --fc-page-bg-color: #f5e6c8;
    --fc-button-bg-color: #d4b896;
    --fc-button-border-color: #c4a882;
    --fc-button-hover-bg-color: #c4a882;
    --fc-button-hover-border-color: #b89880;
    --fc-button-active-bg-color: #d97706;
    --fc-button-active-border-color: #d97706;
    --fc-button-text-color: #3d2b1f;
    --fc-today-bg-color: rgba(217,119,6,0.12);
    --fc-neutral-bg-color: rgba(245,230,200,0.6);
    --fc-neutral-text-color: #5c3d2e;
    --fc-list-event-hover-bg-color: rgba(212,184,150,0.5);
    --fc-event-text-color: #fff;
    color: #3d2b1f;
}
/* Force light backgrounds on calendar cells and elements */
html:not(.dark) .fc .fc-scrollgrid,
html:not(.dark) .fc .fc-scrollgrid-section > td,
html:not(.dark) .fc .fc-daygrid-body,
html:not(.dark) .fc .fc-daygrid-day,
html:not(.dark) .fc .fc-col-header,
html:not(.dark) .fc .fc-col-header-cell,
html:not(.dark) .fc .fc-list,
html:not(.dark) .fc .fc-list-table,
html:not(.dark) .fc .fc-list-day th,
html:not(.dark) .fc .fc-list-event td,
html:not(.dark) .fc table,
html:not(.dark) .fc td,
html:not(.dark) .fc th { background-color: #f5e6c8 !important; }
html:not(.dark) .fc .fc-daygrid-day.fc-day-today { background-color: rgba(217,119,6,0.12) !important; }
html:not(.dark) .fc .fc-col-header-cell-cushion,
html:not(.dark) .fc .fc-daygrid-day-number,
html:not(.dark) .fc .fc-list-day-text,
html:not(.dark) .fc .fc-list-day-side-text { color: #7a5c4a; }
html:not(.dark) .fc .fc-toolbar-title { color: #2c1810; }
html:not(.dark) .fc .fc-button-primary:not(:disabled).fc-button-active { color: #f5e6c8; }

/* ================================================================
   RENDERED MARKDOWN TYPOGRAPHY — .note-body
   Tailwind's preflight strips ALL default element styling (headings
   inherit body size/weight, links lose color/underline, lists lose
   bullets), so every surface that renders markdown needs these rules
   back explicitly. .note-body is the site-wide prose class (notes,
   The Story So Far, the composer's Preview pane — issue #172).
   campaign_export.html carries its own copy — it must stay
   self-contained offline. Dark values first (templates are authored
   dark-first), parchment overrides below.
   ================================================================ */
.note-body h1 { font-size: 1.5rem; font-weight: 700; color: #f5f5f4; margin-top: 0.75em; margin-bottom: 0.375em; }
.note-body h2 { font-size: 1.25rem; font-weight: 600; color: #f5f5f4; margin-top: 0.75em; margin-bottom: 0.375em; }
.note-body h3 { font-size: 1.1rem; font-weight: 600; color: #f5f5f4; margin-top: 0.75em; margin-bottom: 0.375em; }
.note-body p { margin-bottom: 0.5em; }
.note-body a { color: #f59e0b; text-decoration: underline; }
.note-body code { background: rgba(120, 113, 108, 0.3); padding: 0.1em 0.3em; border-radius: 0.25em; font-size: 0.875em; }
.note-body pre { background: rgba(28, 25, 23, 0.6); padding: 0.75em; border-radius: 0.5em; overflow-x: auto; margin-bottom: 0.75em; }
.note-body blockquote { border-left: 3px solid #b45309; padding-left: 1em; color: #a8a29e; font-style: italic; margin-bottom: 0.5em; }
.note-body ul, .note-body ol { padding-left: 1.5em; margin-bottom: 0.5em; }
.note-body ul li { list-style-type: disc; }
.note-body ol li { list-style-type: decimal; }
.note-body li { margin-bottom: 0.25em; }
.note-body strong { font-weight: 600; }

html:not(.dark) .note-body h1, html:not(.dark) .note-body h2, html:not(.dark) .note-body h3 { color: #292524; }
html:not(.dark) .note-body p, html:not(.dark) .note-body li { color: #44403c; }
html:not(.dark) .note-body a { color: #b45309; }
html:not(.dark) .note-body code { background: rgba(120, 113, 108, 0.15); color: #292524; }
html:not(.dark) .note-body pre { background: #f5f5f4; color: #292524; }
html:not(.dark) .note-body blockquote { color: #78716c; }

/* ================================================================
   TALE BODY — rendered story markdown on /tales (the spotlight
   list and the story detail page). Plain CSS is invisible to
   test_light_mode_contrast.py, so the dark values and the
   parchment overrides below must stay hand-matched — same
   structure as .note-body above.
   ================================================================ */
.tale-body h1 { font-size: 1.5rem; font-weight: 700; color: #f5f5f4; margin-top: 1em; margin-bottom: 0.5em; }
.tale-body h2 { font-size: 1.25rem; font-weight: 600; color: #f5f5f4; margin-top: 1em; margin-bottom: 0.5em; }
.tale-body h3 { font-size: 1.1rem; font-weight: 600; color: #f5f5f4; margin-top: 1em; margin-bottom: 0.5em; }
.tale-body p { margin-bottom: 0.75em; }
.tale-body a { color: #f59e0b; text-decoration: underline; }
.tale-body strong { font-weight: 600; }
.tale-body code { background: rgba(120, 113, 108, 0.3); padding: 0.1em 0.3em; border-radius: 0.25em; font-size: 0.875em; }
.tale-body pre { background: rgba(28, 25, 23, 0.6); padding: 1em; border-radius: 0.5em; overflow-x: auto; margin-bottom: 1em; }
.tale-body blockquote { border-left: 3px solid #b45309; padding-left: 1em; color: #a8a29e; font-style: italic; margin-bottom: 0.75em; }
.tale-body ul, .tale-body ol { padding-left: 1.5em; margin-bottom: 0.75em; }
.tale-body ul li { list-style-type: disc; }
.tale-body ol li { list-style-type: decimal; }
.tale-body li { margin-bottom: 0.25em; }

html:not(.dark) .tale-body h1, html:not(.dark) .tale-body h2, html:not(.dark) .tale-body h3 { color: #292524; }
html:not(.dark) .tale-body p, html:not(.dark) .tale-body li { color: #44403c; }
html:not(.dark) .tale-body a { color: #b45309; }
html:not(.dark) .tale-body code { background: rgba(120, 113, 108, 0.15); color: #292524; }
html:not(.dark) .tale-body pre { background: #f5f5f4; color: #292524; }
html:not(.dark) .tale-body blockquote { color: #78716c; }

/* ================================================================
   BULLETIN BODY — rendered markdown for a board notice AND for every
   bulletin comment (_macros/bulletin_comments.html), which is also
   how the library, shop and raffle bodies that borrow the class get
   their type.

   Hoisted here out of bulletin/board_detail.html's inline <style> by
   F-221 phase 2, and MERGED with the orphan parchment-only block that
   used to sit up among the light-mode remaps. That split was the exact
   shape of August bug #2 (`docs/design_drift_audit.md` §1, the
   `.tale-body` hoist): the dark ramp existed only inside the detail
   page's <style>, so the same class on every OTHER page — every
   comment body, the library and shop descriptions — rendered with no
   dark rules at all. Where the two halves disagreed (h2/h3) the inline
   values win: they are what the detail page has always rendered.

   Plain CSS is invisible to tests/test_light_mode_contrast.py, so the
   dark values and the parchment overrides below must stay hand-matched
   — same structure as .note-body and .tale-body above. Paragraph SIZE
   is deliberately not set here: the article caps its own measure and
   sets text-[15.5px] on the wrapper, while a comment body stays at its
   text-sm.
   ================================================================ */
.bulletin-body h1 { font-size: 1.5rem; font-weight: 700; margin-bottom: 0.75rem; color: rgb(251 191 36); font-family: 'Cinzel', serif; }
.bulletin-body h2 { font-size: 1.25rem; font-weight: 600; margin-bottom: 0.625rem; color: rgb(245 158 11); }
.bulletin-body h3 { font-size: 1.1rem; font-weight: 600; margin-bottom: 0.5rem; color: rgb(252 211 77); }
.bulletin-body p  { margin-bottom: 0.875rem; color: rgb(214 211 209); line-height: 1.75; }
.bulletin-body ul, .bulletin-body ol { padding-left: 1.5rem; margin-bottom: 0.875rem; color: rgb(214 211 209); }
.bulletin-body li { margin-bottom: 0.25rem; line-height: 1.6; }
.bulletin-body ul li { list-style-type: disc; }
.bulletin-body ol li { list-style-type: decimal; }
.bulletin-body a  { color: rgb(251 191 36); text-decoration: underline; }
.bulletin-body a:hover { color: rgb(252 211 77); }
.bulletin-body strong { color: rgb(231 229 228); font-weight: 600; }
.bulletin-body em { color: rgb(214 211 209); font-style: italic; }
.bulletin-body code { background: rgb(28 25 23); padding: 0.15rem 0.35rem; border-radius: 0.25rem; font-size: 0.85em; color: rgb(251 191 36); }
.bulletin-body pre  { background: rgb(28 25 23); border: 1px solid rgb(68 64 60); padding: 1rem; border-radius: 0.5rem; overflow-x: auto; margin-bottom: 0.875rem; }
.bulletin-body pre code { background: none; padding: 0; color: rgb(214 211 209); font-size: 0.875rem; }
.bulletin-body blockquote { border-left: 3px solid rgb(120 113 108); padding-left: 1rem; margin-left: 0; margin-bottom: 0.875rem; color: rgb(168 162 158); font-style: italic; }
.bulletin-body hr { border-color: rgb(68 64 60); margin: 1.25rem 0; }
.bulletin-body table { width: 100%; border-collapse: collapse; margin-bottom: 0.875rem; font-size: 0.9rem; }
.bulletin-body th { background: rgb(41 37 36); padding: 0.5rem 0.75rem; text-align: left; color: rgb(214 211 209); font-weight: 600; border: 1px solid rgb(68 64 60); }
.bulletin-body td { padding: 0.5rem 0.75rem; color: rgb(214 211 209); border: 1px solid rgb(68 64 60); }
.bulletin-body tr:nth-child(even) td { background: rgb(28 25 23); }

html:not(.dark) .bulletin-body h1 { color: #7c2d12; }
html:not(.dark) .bulletin-body h2 { color: #b45309; }
html:not(.dark) .bulletin-body h3 { color: #7c2d12; }
html:not(.dark) .bulletin-body p,
html:not(.dark) .bulletin-body ul,
html:not(.dark) .bulletin-body ol { color: #5c3d2e; }
html:not(.dark) .bulletin-body li { color: #5c3d2e; }
html:not(.dark) .bulletin-body a  { color: #7c2d12; }
html:not(.dark) .bulletin-body a:hover { color: #78350f; }
html:not(.dark) .bulletin-body strong { color: #3d2b1f; }
html:not(.dark) .bulletin-body em { color: #5c3d2e; }
html:not(.dark) .bulletin-body code { background: #e8d5a3; color: #7c2d12; }
html:not(.dark) .bulletin-body pre { background: #e8d5a3; border-color: #c4a882; }
html:not(.dark) .bulletin-body pre code { color: #5c3d2e; }
html:not(.dark) .bulletin-body blockquote { border-left-color: #c4a882; color: #9b7b6a; }
html:not(.dark) .bulletin-body hr { border-color: #c4a882; }
html:not(.dark) .bulletin-body th { background: #d4b896; color: #3d2b1f; border-color: #c4a882; }
html:not(.dark) .bulletin-body td { color: #5c3d2e; border-color: #c4a882; }
html:not(.dark) .bulletin-body tr:nth-child(even) td { background: #e8d5a3; }

/* ================================================================
   PARCHMENT LIGHT MODE — extended coverage
   Light/parchment mode is a hand-maintained remap of dark utility
   classes (the templates are authored dark-first; there are no
   `dark:` variants). Because the remap is an explicit table, every
   light-toned text class and every dark background class used in a
   template must be listed here or it renders with its dark value on
   parchment (light text → invisible; dark bg → muddy). Opacity
   suffixes (`/70`, `/30`) are SEPARATE selectors from the base class,
   so they must be listed individually.
   tests/test_light_mode_contrast.py fails the build if a template
   uses a text-/bg- color class that is neither remapped here nor on
   that test's allowlist — that is the guard against silent regressions.
   ================================================================ */

/* --- Light text shades + opacity variants (would be invisible on parchment) --- */
html:not(.dark) .text-stone-50       { color: #3d2b1f !important; }
html:not(.dark) .text-amber-100\/70  { color: #7a5c4a !important; }
html:not(.dark) .text-amber-200,
html:not(.dark) .text-amber-200\/70,
html:not(.dark) .text-amber-200\/80  { color: #7a5c4a !important; }
html:not(.dark) .text-amber-300,
html:not(.dark) .text-amber-300\/80  { color: #b45309 !important; }
html:not(.dark) .text-amber-500\/60,
html:not(.dark) .text-amber-500\/70,
html:not(.dark) .text-amber-500\/80,
html:not(.dark) .text-amber-500\/90  { color: #b45309 !important; }
html:not(.dark) .text-red-50,
html:not(.dark) .text-red-100        { color: #991b1b !important; }
html:not(.dark) .text-red-500,
html:not(.dark) .text-red-500\/60    { color: #b91c1c !important; }
html:not(.dark) .text-emerald-500\/70 { color: #047857 !important; }
html:not(.dark) .text-red-300\/80    { color: #b91c1c !important; }
html:not(.dark) .text-green-100,
html:not(.dark) .text-green-200      { color: #065f46 !important; }
html:not(.dark) .text-green-400      { color: #047857 !important; }
html:not(.dark) .text-emerald-50     { color: #065f46 !important; }
html:not(.dark) .text-blue-100       { color: #1e3a5f !important; }
html:not(.dark) .text-blue-400       { color: #1e40af !important; }
html:not(.dark) .text-cyan-400       { color: #0e7490 !important; }
html:not(.dark) .text-violet-300,
html:not(.dark) .text-violet-400     { color: #6d28d9 !important; }
html:not(.dark) .text-rose-400       { color: #be123c !important; }
html:not(.dark) .text-rose-300       { color: #be123c !important; }
html:not(.dark) .text-yellow-400     { color: #a16207 !important; }
html:not(.dark) .text-orange-400     { color: #c2410c !important; }

/* --- Solid colored buttons: keep light text. The global `.text-white`
   remap above flips white → near-black, which is correct on parchment
   surfaces but destroys solid colored buttons (e.g. a red delete button
   would become dark-text-on-dark-red). The compound selectors below have
   higher specificity than the bare `.text-white` rule, so they win and
   restore white only where the text sits on a saturated button. --- */
html:not(.dark) .bg-red-500.text-white,
html:not(.dark) .bg-red-600.text-white,
html:not(.dark) .bg-red-700.text-white,
html:not(.dark) .bg-emerald-600.text-white,
html:not(.dark) .bg-indigo-600.text-white,
html:not(.dark) .bg-amber-600.text-white   { color: #fff !important; }
html:not(.dark) .bg-amber-700.text-amber-50 { color: #fdf6e3 !important; }

/* --- Translucent dark washes / chips → light parchment tints --- */
html:not(.dark) .bg-stone-800\/20  { background-color: rgba(232,213,163,0.20) !important; }
html:not(.dark) .bg-stone-800\/30  { background-color: rgba(232,213,163,0.30) !important; }
html:not(.dark) .bg-stone-800\/60  { background-color: rgba(232,213,163,0.60) !important; }
html:not(.dark) .bg-stone-800\/70  { background-color: rgba(232,213,163,0.70) !important; }
html:not(.dark) .bg-stone-700\/30  { background-color: rgba(212,184,150,0.30) !important; }
html:not(.dark) .bg-stone-700\/40  { background-color: rgba(212,184,150,0.40) !important; }
html:not(.dark) .bg-stone-700\/70  { background-color: rgba(212,184,150,0.70) !important; }
html:not(.dark) .bg-stone-900\/30  { background-color: rgba(245,230,200,0.30) !important; }
html:not(.dark) .bg-stone-900\/60  { background-color: rgba(245,230,200,0.60) !important; }
html:not(.dark) .bg-stone-900\/70  { background-color: rgba(245,230,200,0.70) !important; }
/* F-219: the night roster's phone tap-to-seat sheet. */
html:not(.dark) .bg-stone-900\/97  { background-color: rgba(245,230,200,0.97) !important; }
html:not(.dark) .bg-stone-600      { background-color: #c4a882 !important; }
html:not(.dark) .bg-stone-500\/70  { background-color: rgba(155,123,106,0.70) !important; }
html:not(.dark) .bg-amber-500\/10  { background-color: rgba(217,119,6,0.10) !important; }
html:not(.dark) .bg-amber-800\/40  { background-color: rgba(217,119,6,0.13) !important; }
html:not(.dark) .bg-amber-800\/60  { background-color: rgba(217,119,6,0.15) !important; }
html:not(.dark) .bg-amber-900\/10  { background-color: rgba(217,119,6,0.05) !important; }
html:not(.dark) .bg-amber-900\/20  { background-color: rgba(217,119,6,0.07) !important; }
html:not(.dark) .bg-amber-900\/30  { background-color: rgba(217,119,6,0.10) !important; }
html:not(.dark) .bg-amber-950\/10  { background-color: rgba(217,119,6,0.05) !important; }
html:not(.dark) .bg-amber-950\/30  { background-color: rgba(217,119,6,0.10) !important; }
html:not(.dark) .bg-amber-950\/50  { background-color: rgba(217,119,6,0.13) !important; }
html:not(.dark) .bg-red-800        { background-color: rgba(239,68,68,0.18) !important; }
html:not(.dark) .bg-red-900\/50    { background-color: rgba(239,68,68,0.13) !important; }
html:not(.dark) .bg-red-900\/80    { background-color: rgba(239,68,68,0.16) !important; }
html:not(.dark) .bg-red-950\/10    { background-color: rgba(239,68,68,0.05) !important; }
html:not(.dark) .bg-red-950\/30    { background-color: rgba(239,68,68,0.10) !important; }
html:not(.dark) .bg-green-900\/20  { background-color: rgba(16,185,129,0.07) !important; }
html:not(.dark) .bg-green-900\/30  { background-color: rgba(16,185,129,0.10) !important; }
html:not(.dark) .bg-green-900\/50  { background-color: rgba(16,185,129,0.13) !important; }
html:not(.dark) .bg-emerald-900\/20 { background-color: rgba(16,185,129,0.07) !important; }
html:not(.dark) .bg-emerald-900\/30 { background-color: rgba(16,185,129,0.10) !important; }
html:not(.dark) .bg-emerald-900\/50 { background-color: rgba(16,185,129,0.13) !important; }
html:not(.dark) .bg-emerald-950\/25 { background-color: rgba(16,185,129,0.09) !important; }
html:not(.dark) .bg-emerald-950\/30 { background-color: rgba(16,185,129,0.10) !important; }
html:not(.dark) .bg-blue-900\/30   { background-color: rgba(59,130,246,0.10) !important; }
html:not(.dark) .bg-cyan-900\/30   { background-color: rgba(8,145,178,0.10) !important; }
html:not(.dark) .bg-purple-900\/20 { background-color: rgba(139,92,246,0.07) !important; }
html:not(.dark) .bg-purple-900\/30 { background-color: rgba(139,92,246,0.10) !important; }
html:not(.dark) .bg-purple-900\/50 { background-color: rgba(139,92,246,0.13) !important; }
html:not(.dark) .bg-purple-950\/20 { background-color: rgba(139,92,246,0.07) !important; }
html:not(.dark) .bg-violet-900\/30 { background-color: rgba(139,92,246,0.10) !important; }
html:not(.dark) .bg-rose-900\/30   { background-color: rgba(244,63,94,0.10) !important; }
html:not(.dark) .bg-orange-900\/30 { background-color: rgba(234,88,12,0.10) !important; }
html:not(.dark) .bg-yellow-900\/30 { background-color: rgba(202,138,4,0.12) !important; }

/* --- Selected/toggle buttons (translucent dark colour + light colour text):
   light tint bg so the (now-dark) text reads. Includes hover/active states. --- */
html:not(.dark) .bg-blue-700\/60,
html:not(.dark) .hover\:bg-blue-700:hover  { background-color: rgba(59,130,246,0.18) !important; }
html:not(.dark) .bg-green-700\/60,
html:not(.dark) .hover\:bg-green-700:hover { background-color: rgba(16,185,129,0.18) !important; }

/* --- Gradient stops on admin attention cards (would stay dark otherwise).
   Each `from-*` utility sets both --tw-gradient-from and --tw-gradient-stops,
   so the stops var is restated to pick up the lightened colors. --- */
html:not(.dark) .from-red-950\/40 {
    --tw-gradient-from: rgba(239,68,68,0.10) var(--tw-gradient-from-position) !important;
    --tw-gradient-to: rgba(239,68,68,0) var(--tw-gradient-to-position) !important;
    --tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to) !important;
}
html:not(.dark) .hover\:from-red-950\/60:hover {
    --tw-gradient-from: rgba(239,68,68,0.16) var(--tw-gradient-from-position) !important;
    --tw-gradient-to: rgba(239,68,68,0) var(--tw-gradient-to-position) !important;
    --tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to) !important;
}
html:not(.dark) .from-amber-950\/40 {
    --tw-gradient-from: rgba(217,119,6,0.10) var(--tw-gradient-from-position) !important;
    --tw-gradient-to: rgba(217,119,6,0) var(--tw-gradient-to-position) !important;
    --tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to) !important;
}
html:not(.dark) .hover\:from-amber-950\/60:hover {
    --tw-gradient-from: rgba(217,119,6,0.16) var(--tw-gradient-from-position) !important;
    --tw-gradient-to: rgba(217,119,6,0) var(--tw-gradient-to-position) !important;
    --tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to) !important;
}
html:not(.dark) .to-stone-900\/40 {
    --tw-gradient-to: rgba(245,230,200,0.40) var(--tw-gradient-to-position) !important;
}
/* The Story So Far's collapsed-fade (tables/_story_so_far.html, F-218): a
   fade to the parchment CARD colour, not to stone. */
html:not(.dark) .from-stone-800 {
    --tw-gradient-from: #e8d5a3 var(--tw-gradient-from-position) !important;
    --tw-gradient-to: rgba(232,213,163,0) var(--tw-gradient-to-position) !important;
    --tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to) !important;
}

/* ================================================================
   TALES CATEGORY TILES / BAND / BADGES (/tales/ feed)
   The per-category accent rides the --cat custom property, set by
   the shared cat--* classes used by the tiles, the active-category
   band and the story-type badges. Surfaces sit on the app's
   stone/amber system (tool-tile tier — see DESIGN.md); plain CSS
   is invisible to test_light_mode_contrast.py, so dark values
   first and the parchment overrides stay hand-matched.
   ================================================================ */

.cat--all            { --cat: #e0a233; }
.cat--session_recap  { --cat: #5b93c9; }
.cat--character_lore { --cat: #a884d0; }
.cat--world_lore     { --cat: #63b189; }
.cat--obituary       { --cat: #cf6b6b; }
html:not(.dark) .cat--all            { --cat: #a16207; }
html:not(.dark) .cat--session_recap  { --cat: #2f6797; }
html:not(.dark) .cat--character_lore { --cat: #6d4fa3; }
html:not(.dark) .cat--world_lore     { --cat: #2e7d57; }
html:not(.dark) .cat--obituary       { --cat: #a94444; }

/* --- Category tile deck (tab row) --- */
.cat-tiles {
    display: flex;
    flex-wrap: wrap;
    gap: 10px;
}
.cat-tile {
    flex: 1 1 0;
    min-height: 104px;
    padding: 14px 16px;
    display: flex;
    flex-direction: column;
    gap: 4px;
    border-radius: 0.75rem;                 /* rounded-xl */
    background: rgb(41 37 36 / 0.4);        /* bg-stone-800/40 */
    border: 1px solid rgb(68 64 60 / 0.4);  /* border-stone-700/40 */
    transition: border-color 0.15s ease, background-color 0.15s ease;
}
.cat-tile:hover {
    border-color: var(--cat);
}
.cat-tile.is-active {
    background: rgb(41 37 36 / 0.8);        /* bg-stone-800/80 */
    border-color: var(--cat);
    box-shadow: inset 0 3px 0 var(--cat);
}
html:not(.dark) .cat-tile {
    background: #e8d5a3;
    border-color: #c4a882;
}
html:not(.dark) .cat-tile.is-active {
    background: #d4b896;
}
.cat-tile-dot {
    width: 8px;
    height: 8px;
    border-radius: 9999px;
    flex-shrink: 0;
    background: var(--cat);
}
.cat-tile-name {
    font-family: 'Cinzel', serif;
    font-size: 1rem;
    font-weight: 700;   /* Cinzel is loaded at 400/700 only */
    color: #e7e5e4;     /* text-stone-200 */
}
html:not(.dark) .cat-tile-name { color: #3d2b1f; }
.cat-tile-desc {
    font-size: 0.75rem; /* text-xs */
    line-height: 1.35;
    color: #a8a29e;     /* text-stone-400 */
}
html:not(.dark) .cat-tile-desc { color: #7a5c4a; }
.cat-tile-count {
    margin-top: auto;
    font-size: 10px;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.1em; /* tracking-widest */
    color: var(--cat);
}
@media (max-width: 640px) {
    /* Slim single-line rows on phones (name left, count right). The tall 2-up
       tiles filled the whole viewport and hid the list they filter (#156); the
       description is dropped here because the active-category band right below
       the deck repeats it. */
    .cat-tiles { gap: 6px; }
    .cat-tile {
        flex: 1 1 100%;
        min-height: 0;
        flex-direction: row;
        align-items: center;
        justify-content: space-between;
        gap: 8px;
        padding: 9px 13px;
    }
    .cat-tile-desc { display: none; }
    .cat-tile-count { margin-top: 0; white-space: nowrap; }
    .cat-tile-name { font-size: 0.875rem; white-space: nowrap; }
}

/* --- Active-category band (title + description + search) --- */
.cat-band {
    border: 1px solid rgb(68 64 60 / 0.4);  /* border-stone-700/40 */
    border-left: 4px solid var(--cat);
    border-radius: 0.75rem;
    padding: 14px 20px;
    display: flex;
    justify-content: space-between;
    align-items: center;
    gap: 16px;
    background: rgb(41 37 36 / 0.4);        /* bg-stone-800/40 */
}
html:not(.dark) .cat-band {
    background: #e8d5a3;
    border-color: #c4a882;
    border-left-color: var(--cat);
}
.cat-band-title {
    font-family: 'Cinzel', serif;
    font-size: 1.125rem; /* text-lg card-title tier */
    font-weight: 700;
    color: #f5f5f4;      /* text-stone-100 */
}
html:not(.dark) .cat-band-title { color: #3d2b1f; }
.cat-band-desc {
    font-size: 0.75rem;
    color: #a8a29e;
}
html:not(.dark) .cat-band-desc { color: #7a5c4a; }
@media (max-width: 640px) {
    /* Search stacks below the title on phones. */
    .cat-band {
        flex-direction: column;
        align-items: stretch;
    }
}

/* --- Story-type badge (feed cards + My Stories) ---
   Mirrors the canonical Tailwind badge (px-2 py-0.5 text-[10px]
   font-bold uppercase tracking-widest rounded + border) with the
   colour riding --cat. --- */
.tale-badge {
    display: inline-block;
    padding: 2px 8px;
    border-radius: 0.25rem; /* rounded */
    font-size: 10px;
    font-weight: 700;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    color: var(--cat);
    background: rgba(0, 0, 0, 0.06); /* fallback for browsers without color-mix */
    background: color-mix(in srgb, var(--cat) 15%, transparent);
    border: 1px solid transparent;   /* fallback */
    border-color: color-mix(in srgb, var(--cat) 30%, transparent);
}

/* --- Reusable admin table -> cards on mobile (#183) ------------------------
   Add class `admin-cards` to a wide admin <table>; on phones (< sm) each row
   reflows into a card (same DOM, so AJAX pagination / sorting keep working;
   desktop is unchanged). Annotate cells:
     data-card="title"   -> prominent full-width heading
     data-card="meta"    -> inline badge/short value, no label
     data-card="actions" -> full-width action row at the card foot
     data-card="hide"    -> desktop-only (hidden on mobile)
     data-label="Label"  -> full-width line prefixed with an uppercase caption
   Un-annotated cells fall back to a plain full-width line. */
@media (max-width: 639px) {
  table.admin-cards,
  table.admin-cards > tbody { display: block; }
  table.admin-cards > thead { display: none; }
  table.admin-cards { min-width: 0 !important; }
  table.admin-cards > tbody > tr {
    display: flex; flex-wrap: wrap; align-items: baseline; gap: 0.3rem 0.6rem;
    margin: 0.5rem 0.75rem; padding: 0.85rem 1rem;
    border: 1px solid rgb(68 64 60 / 0.6); border-radius: 0.75rem;
  }
  table.admin-cards > tbody > tr > td {
    display: block; order: 3; flex-basis: 100%;
    width: auto !important; max-width: none !important; position: static !important;
    padding: 0; border: 0 !important; white-space: normal; font-size: 0.8rem; color: #d6d3d1;
  }
  table.admin-cards td[data-card="title"] {
    order: 0; font-weight: 700; font-size: 0.95rem; color: #e7e5e4; word-break: break-word;
  }
  table.admin-cards td[data-card="meta"] {
    order: 2; display: inline-flex; flex: 0 0 auto; flex-basis: auto; flex-wrap: wrap; gap: 0.25rem;
  }
  table.admin-cards td[data-card="hide"] { display: none; }
  table.admin-cards td[data-card="actions"] {
    order: 9; margin-top: 0.5rem; padding-top: 0.6rem;
    border-top: 1px solid rgb(68 64 60 / 0.5) !important; text-align: left !important;
  }
  table.admin-cards td[data-label]::before {
    content: attr(data-label); display: inline-block; min-width: 4.5rem;
    color: #78716c; text-transform: uppercase; font-size: 0.62rem; letter-spacing: 0.04em; margin-right: 0.4rem;
  }
  table.admin-cards td code { word-break: break-word; }
  table.admin-cards > tbody > tr:has(> td[colspan]) { display: block; border: 0; padding: 0; margin: 0; }
  table.admin-cards > tbody > tr:has(> td[colspan]) > td { display: block; order: 0; flex-basis: auto; }
}

/* ================================================================
   NOTIFICATION BELL — unread jingle (F-128)
   The bell badge reads clearly on mobile but is easy to miss on a wide
   desktop header, so an unread count gives the bell a short swing.

   Deliberately a BURST, not a loop. The unread count is computed
   server-side on every render with no polling (see
   inject_notification_context), so an infinite animation tied to
   "unread > 0" would swing on every page load for as long as the
   notification stays unread — nagging rather than nudging. Three swings
   over ~1.1s draws the eye once and then sits still.
   ================================================================ */
@keyframes bell-jingle {
  0%   { transform: rotate(0deg); }
  15%  { transform: rotate(11deg); }
  30%  { transform: rotate(-9deg); }
  45%  { transform: rotate(7deg); }
  60%  { transform: rotate(-5deg); }
  75%  { transform: rotate(3deg); }
  100% { transform: rotate(0deg); }
}

.bell-jingle {
  /* Swing from the crown, the way a real bell hangs — rotating about the
     centre reads as a wobble instead. */
  transform-origin: 50% 15%;
  animation: bell-jingle 1.1s ease-in-out 1 both;
}

/* Reduced-motion visitors get the attention cue without the movement: the bell
   simply sits in the badge's amber. Mirrors the treatment in
   main/pages/static/js/crypt/index.js.

   The colour is per-theme on purpose. Raw amber-500 (#f59e0b) is the dark-mode
   value; on the cream parchment background it washes out, which is exactly why
   the remap table above rewrites .text-amber-500 to #b45309. A hardcoded
   amber-500 here would sidestep that remap — and the light-mode contrast guard
   can't catch it, because this is a plain CSS colour, not a utility class. */
@media (prefers-reduced-motion: reduce) {
  .bell-jingle {
    animation: none;
    transform: none;
    color: #f59e0b; /* amber-500 — dark mode */
  }
  html:not(.dark) .bell-jingle {
    color: #b45309; /* amber-700 — matches the parchment remap of .text-amber-500 */
  }
}

/* ================================================================
   WORLD WIKI links (F-135, #236)
   [[Wikilinks]] inside rendered page bodies. Two states: .wiki-link
   resolves to an existing page (amber underline); .wiki-link-new is a
   manager-only "create this page" affordance (dashed muted underline).
   Anchors are injected pre-markdown and survive bleach (a[href|class]
   is allowed), so these classes are the whole treatment.

   Plain CSS colours, so the light-mode contrast guard can't see them —
   the parchment values below are hand-matched to the remap table above
   (amber-400 -> #7c2d12 territory, muted stone likewise). The
   .note-body selectors out-rank the generic ".note-body a" colour rule
   further up this file.
   ================================================================ */
.wiki-link,
.note-body a.wiki-link {
  color: #fbbf24; /* amber-400 — dark mode */
  text-decoration: underline;
  text-decoration-color: rgba(251, 191, 36, 0.45);
  text-underline-offset: 2px;
}
.wiki-link:hover,
.note-body a.wiki-link:hover {
  text-decoration-color: #fbbf24;
}
.wiki-link-new,
.note-body a.wiki-link-new {
  color: #a8a29e; /* stone-400 — muted: the page doesn't exist yet */
  text-decoration: underline dashed;
  text-decoration-color: rgba(168, 162, 158, 0.6);
  text-underline-offset: 2px;
}
.wiki-link-new:hover,
.note-body a.wiki-link-new:hover {
  color: #fbbf24;
}
html:not(.dark) .wiki-link,
html:not(.dark) .note-body a.wiki-link {
  color: #7c2d12; /* amber-900 — parchment heading amber */
  text-decoration-color: rgba(146, 64, 14, 0.45);
}
html:not(.dark) .wiki-link:hover,
html:not(.dark) .note-body a.wiki-link:hover {
  text-decoration-color: #7c2d12;
}
html:not(.dark) .wiki-link-new,
html:not(.dark) .note-body a.wiki-link-new {
  color: #78716c; /* stone-500 — parchment muted */
  text-decoration-color: rgba(120, 113, 108, 0.6);
}
html:not(.dark) .wiki-link-new:hover,
html:not(.dark) .note-body a.wiki-link-new:hover {
  color: #7c2d12;
}

/* ================================================================
   THE ATLAS (F-136, #239)
   Leaflet map pages inside the World Wiki: the map frame, the pin
   markers (L.divIcon carries these classes), and a re-skin of the
   stock Leaflet popup into the stone/amber idiom.

   Plain CSS colours throughout — invisible to the light-mode contrast
   guard (it only scans Tailwind utilities in templates), so every
   rule carries a hand-matched html:not(.dark) parchment override
   below, same discipline as the wiki-link block above.
   ================================================================ */
/* Stacking isolation (#289) — Leaflet runs its panes and controls at
   z-index 400–1000 INSIDE the container. Without a stacking context of its
   own, those values compete page-wide and paint over the site chrome: the
   sticky header is z-[35], and the notification bell popup is stacked
   inside that header, so no z-index on the popup itself can ever outbid
   an un-contained pane. `isolation` traps the fight — outside, the whole
   map competes as one z-auto block, below the chrome. Deliberately on the
   generic .leaflet-container (not .atlas-map-frame) so every future map
   page inherits the fix. Guarded by tests/test_notifications.py. */
.leaflet-container {
  isolation: isolate;
}

.atlas-map-frame {
  height: 60vh;
  min-height: 320px;
  background: #1c1917; /* stone-900 — letterboxes around the image */
}
/* Leaflet's stylesheet loads AFTER style.css (head_extra) and paints the
   container #ddd; out-specific it or the letterbox flashes grey. */
.atlas-map-frame.leaflet-container {
  background: #1c1917;
}
html:not(.dark) .atlas-map-frame,
html:not(.dark) .atlas-map-frame.leaflet-container {
  background: #efe9dc; /* parchment letterbox */
}

/* Pin marker — a pole-and-pennant flag drawn as the static inline SVG in
   wiki-atlas.js (#289 follow-up: the old 22px teardrop vanished on busy art).
   The icon box is 26x30 anchored at the pole base. The pennant colour is the
   linked page's wiki group — slots .g0–.g5, assigned deterministically by
   wiki-atlas.js (keep PALETTE_SIZE there in step with the classes here).
   .dm = purple (DM-only), .ghost = dashed hollow (unwritten place,
   manager-only payload); both skip the group palette in JS. */
.atlas-pin {
  cursor: pointer;
}
.atlas-pin svg {
  display: block;
  width: 100%;
  height: 100%;
  overflow: visible;
  filter: drop-shadow(0 1px 3px rgba(0, 0, 0, 0.65));
}
.atlas-flag-pole {
  fill: #1c1917; /* stone-900 */
}
.atlas-flag-pennant {
  fill: #f59e0b; /* g0: amber-500 */
  stroke: #78350f; /* amber-900 */
  stroke-width: 1.5;
  stroke-linejoin: round;
}
.atlas-pin.g1 .atlas-flag-pennant {
  fill: #ef4444; /* red-500 */
  stroke: #7f1d1d;
}
.atlas-pin.g2 .atlas-flag-pennant {
  fill: #10b981; /* emerald-500 */
  stroke: #064e3b;
}
.atlas-pin.g3 .atlas-flag-pennant {
  fill: #0ea5e9; /* sky-500 */
  stroke: #0c4a6e;
}
.atlas-pin.g4 .atlas-flag-pennant {
  fill: #d946ef; /* fuchsia-500 */
  stroke: #701a75;
}
.atlas-pin.g5 .atlas-flag-pennant {
  fill: #84cc16; /* lime-500 */
  stroke: #365314;
}
.atlas-pin.dm .atlas-flag-pennant {
  fill: #a855f7; /* purple-500, the established DM-secret signal */
  stroke: #581c87;
}
.atlas-pin.ghost .atlas-flag-pennant {
  fill: rgba(28, 25, 23, 0.35);
  stroke: #a8a29e; /* stone-400 */
  stroke-dasharray: 3 2;
}
html:not(.dark) .atlas-pin svg {
  filter: drop-shadow(0 1px 3px rgba(68, 64, 60, 0.5));
}
html:not(.dark) .atlas-flag-pole {
  fill: #44403c; /* stone-700 — pure black is harsh on parchment art */
}
/* Parchment pennants drop one step darker so they hold against light art. */
html:not(.dark) .atlas-flag-pennant {
  fill: #d97706; /* amber-600 */
}
html:not(.dark) .atlas-pin.g1 .atlas-flag-pennant {
  fill: #dc2626; /* red-600 */
}
html:not(.dark) .atlas-pin.g2 .atlas-flag-pennant {
  fill: #059669; /* emerald-600 */
}
html:not(.dark) .atlas-pin.g3 .atlas-flag-pennant {
  fill: #0284c7; /* sky-600 */
}
html:not(.dark) .atlas-pin.g4 .atlas-flag-pennant {
  fill: #c026d3; /* fuchsia-600 */
}
html:not(.dark) .atlas-pin.g5 .atlas-flag-pennant {
  fill: #65a30d; /* lime-600 */
}
html:not(.dark) .atlas-pin.dm .atlas-flag-pennant {
  fill: #9333ea; /* purple-600 */
}
html:not(.dark) .atlas-pin.ghost .atlas-flag-pennant {
  fill: rgba(245, 240, 230, 0.5);
  stroke: #78716c;
}

/* Legend — the L.Control wiki-atlas.js builds: pennant colour → wiki group
   (+ counts). Collapsible ("Legend" pill toggles the rows; starts folded on
   phones). Same stone/amber panel idiom as the popup re-skin below. */
.atlas-legend {
  background: rgba(28, 25, 23, 0.92); /* stone-900/92 */
  border: 1px solid rgba(146, 64, 14, 0.4); /* amber-900/40 */
  border-radius: 0.6rem;
  padding: 0.35rem 0.5rem;
  max-width: 13rem;
}
.atlas-legend-toggle {
  display: block;
  width: 100%;
  text-align: left;
  font-size: 10px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  color: #f59e0b; /* amber-500 */
  background: none;
  border: 0;
  padding: 0.15rem 0.1rem;
  cursor: pointer;
}
.atlas-legend.collapsed .atlas-legend-rows {
  display: none;
}
.atlas-legend-row {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  padding: 0.15rem 0.1rem;
}
.atlas-legend-swatch {
  width: 14px;
  height: 16px;
  flex-shrink: 0;
  cursor: default;
}
.atlas-legend-swatch svg {
  filter: none;
}
.atlas-legend-label {
  font-size: 11px;
  color: #d6d3d1; /* stone-300 */
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.atlas-legend-count {
  font-size: 10px;
  color: #a8a29e; /* stone-400 */
  margin-left: auto;
}
html:not(.dark) .atlas-legend {
  background: rgba(253, 250, 243, 0.94); /* parchment panel */
  border-color: #d6cdb8;
}
html:not(.dark) .atlas-legend-toggle {
  color: #b45309; /* amber-700 */
}
html:not(.dark) .atlas-legend-label {
  color: #44403c;
}
html:not(.dark) .atlas-legend-count {
  color: #78716c;
}

/* Leaflet popup re-skin — stone-800 wrapper, amber accents. */
.atlas-popup .leaflet-popup-content-wrapper {
  background: #292524; /* stone-800 */
  color: #d6d3d1; /* stone-300 */
  border: 1px solid rgba(146, 64, 14, 0.4); /* amber-900/40 */
  border-radius: 0.75rem;
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.5);
}
.atlas-popup .leaflet-popup-tip {
  background: #292524;
  border: 1px solid rgba(146, 64, 14, 0.4);
}
.atlas-popup .leaflet-popup-content {
  margin: 0.75rem 1rem;
}
.atlas-popup a.leaflet-popup-close-button {
  color: #a8a29e;
}
.atlas-popup-eyebrow {
  font-size: 10px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  color: #f59e0b; /* amber-500 */
  margin: 0 0 0.15rem;
}
.atlas-popup-title {
  font-family: "Cinzel", Georgia, serif;
  font-weight: 600;
  color: #f5f5f4; /* stone-100 */
  margin: 0 0 0.35rem;
}
.atlas-popup .atlas-popup-link {
  color: #fbbf24; /* amber-400 */
  font-size: 0.8rem;
  text-decoration: underline;
  text-underline-offset: 2px;
}
html:not(.dark) .atlas-popup .leaflet-popup-content-wrapper,
html:not(.dark) .atlas-popup .leaflet-popup-tip {
  background: #fdfaf3; /* parchment panel */
  color: #44403c;
  border-color: #d6cdb8;
}
html:not(.dark) .atlas-popup a.leaflet-popup-close-button {
  color: #78716c;
}
html:not(.dark) .atlas-popup-eyebrow {
  color: #b45309; /* amber-700 — the parchment remap of amber-500 */
}
html:not(.dark) .atlas-popup-title {
  color: #292524;
}
html:not(.dark) .atlas-popup .atlas-popup-link {
  color: #7c2d12;
}

/* ================================================================
   Atlas pin editor (F-136 part 2, #239) — the edit-mode popover
   wiki-atlas.js builds inside #atlas-map, plus the placing-cursor
   and temp-pin states. Same plain-CSS + parchment-override
   discipline as the atlas block above.
   ================================================================ */
/* Crosshair while placing — the two-class selector outbids Leaflet's
   .leaflet-grab cursor rule on the same element. */
.atlas-map-frame.atlas-placing {
  cursor: crosshair;
}
/* The not-yet-saved pin under the open "New pin" popover. */
.atlas-pin.temp {
  opacity: 0.7;
}
.atlas-editor {
  position: absolute;
  z-index: 900; /* above every Leaflet pane (popups 700, controls 800) */
  width: 16.5rem;
  background: #1c1917; /* stone-900 */
  border: 1px solid rgba(146, 64, 14, 0.4); /* amber-900/40 */
  border-radius: 0.75rem;
  padding: 0.75rem;
  box-shadow: 0 10px 28px rgba(0, 0, 0, 0.55);
}
.atlas-editor-heading {
  font-size: 10px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  color: #f59e0b; /* amber-500 */
  margin: 0 0 0.5rem;
}
.atlas-editor-input {
  display: block;
  width: 100%;
  margin-bottom: 0.4rem;
  padding: 0.4rem 0.6rem;
  font-size: 0.8rem;
  color: #f5f5f4; /* stone-100 */
  background: #292524; /* stone-800 */
  border: 1px solid rgba(87, 83, 78, 0.6); /* stone-600/60 */
  border-radius: 0.5rem;
}
.atlas-editor-input:focus {
  outline: 2px solid #f59e0b;
  outline-offset: -1px;
}
.atlas-editor-results {
  max-height: 9rem;
  overflow-y: auto;
  margin-bottom: 0.4rem;
}
.atlas-editor-results:empty {
  margin-bottom: 0;
}
.atlas-editor-result {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 0.5rem;
  width: 100%;
  text-align: left;
  padding: 0.3rem 0.5rem;
  border-radius: 0.4rem;
  font-size: 0.8rem;
  color: #d6d3d1; /* stone-300 */
  background: transparent;
}
.atlas-editor-result:hover {
  background: rgba(146, 64, 14, 0.25);
  color: #fbbf24; /* amber-400 */
}
.atlas-editor-result-group {
  font-size: 0.65rem;
  color: #78716c; /* stone-500 */
  white-space: nowrap;
}
.atlas-editor-dm {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  font-size: 0.75rem;
  color: #d6d3d1;
  margin: 0.2rem 0 0.6rem;
  cursor: pointer;
}
.atlas-editor-buttons {
  display: flex;
  align-items: center;
  gap: 0.5rem;
}
.atlas-editor-save,
.atlas-editor-cancel,
.atlas-editor-delete {
  font-size: 0.75rem;
  font-weight: 600;
  padding: 0.35rem 0.8rem;
  border-radius: 0.5rem;
}
.atlas-editor-save {
  background: rgba(217, 119, 6, 0.8); /* amber-600/80 */
  color: #1c1917;
}
.atlas-editor-save:hover {
  background: #f59e0b;
}
.atlas-editor-cancel {
  color: #a8a29e; /* stone-400 */
  border: 1px solid rgba(87, 83, 78, 0.6);
}
.atlas-editor-cancel:hover {
  color: #f5f5f4;
}
.atlas-editor-delete {
  margin-left: auto;
  color: #f87171; /* red-400 */
  border: 1px solid rgba(153, 27, 27, 0.5); /* red-800/50 */
}
.atlas-editor-delete:hover {
  background: rgba(153, 27, 27, 0.3);
  color: #fecaca;
}
html:not(.dark) .atlas-editor {
  background: #fdfaf3; /* parchment panel */
  border-color: #d6cdb8;
  box-shadow: 0 10px 28px rgba(68, 64, 60, 0.35);
}
html:not(.dark) .atlas-editor-heading {
  color: #b45309; /* amber-700 */
}
html:not(.dark) .atlas-editor-input {
  color: #292524;
  background: #f5f0e6;
  border-color: #d6cdb8;
}
html:not(.dark) .atlas-editor-result {
  color: #44403c;
}
html:not(.dark) .atlas-editor-result:hover {
  background: rgba(180, 83, 9, 0.12);
  color: #7c2d12;
}
html:not(.dark) .atlas-editor-result-group {
  color: #78716c;
}
html:not(.dark) .atlas-editor-dm {
  color: #44403c;
}
html:not(.dark) .atlas-editor-save {
  background: #b45309;
  color: #fdfaf3;
}
html:not(.dark) .atlas-editor-save:hover {
  background: #7c2d12;
}
html:not(.dark) .atlas-editor-cancel {
  color: #78716c;
  border-color: #d6cdb8;
}
html:not(.dark) .atlas-editor-cancel:hover {
  color: #292524;
}
html:not(.dark) .atlas-editor-delete {
  color: #b91c1c;
  border-color: rgba(185, 28, 28, 0.4);
}
html:not(.dark) .atlas-editor-delete:hover {
  background: rgba(185, 28, 28, 0.1);
  color: #7f1d1d;
}

/* ================================================================
   CHAT CLIENT (F-163) — parchment remaps for the Discord-shaped chat.
   Guard-enforced bg-/text- classes plus the border shades the guard
   doesn't watch but parchment needs. chat.css carries the plain-CSS
   (gradient/rgba) side of the same remap — keep both in step.
   ================================================================ */
html:not(.dark) .bg-amber-900\/15  { background-color: rgba(217,119,6,0.06) !important; }
html:not(.dark) .bg-amber-900\/25  { background-color: rgba(217,119,6,0.09) !important; }
html:not(.dark) .bg-amber-900\/35  { background-color: rgba(217,119,6,0.11) !important; }
html:not(.dark) .bg-red-900\/35    { background-color: rgba(239,68,68,0.11) !important; }
html:not(.dark) .bg-red-700\/60    { background-color: rgba(185,28,28,0.45) !important; } /* NEW MESSAGES rule */
html:not(.dark) .bg-emerald-900\/35 { background-color: rgba(16,185,129,0.11) !important; }
html:not(.dark) .bg-stone-950      { background-color: #efdcb2 !important; }
html:not(.dark) .bg-stone-950\/40  { background-color: rgba(239,220,178,0.55) !important; }
/* Presence dot: the same tone .text-stone-500 maps to, visible on cream. */
html:not(.dark) .bg-stone-500      { background-color: #9b7b6a !important; }
/* "Marked as unread" dot in the channel list: near-white on stone, so it needs
   the ink tone on parchment or it disappears into the cream. */
html:not(.dark) .bg-stone-300      { background-color: #4a3527 !important; }
/* Borders (unguarded — hand-kept for parchment legibility). */
html:not(.dark) .border-stone-950     { border-color: #efdcb2 !important; }
html:not(.dark) .border-amber-700\/35 { border-color: rgba(217,119,6,0.33) !important; }
html:not(.dark) .border-amber-700\/50 { border-color: rgba(217,119,6,0.45) !important; }
html:not(.dark) .border-amber-700\/60 { border-color: rgba(217,119,6,0.55) !important; }
html:not(.dark) .border-red-800\/50   { border-color: rgba(239,68,68,0.40) !important; }
html:not(.dark) .border-emerald-800\/55 { border-color: rgba(16,185,129,0.42) !important; }
html:not(.dark) .border-emerald-800\/60 { border-color: rgba(16,185,129,0.45) !important; }

/* ================================================================
   SRD REFERENCE (F-205, #248)
   The /srd section: build-time cross-links inside rendered SRD
   bodies, the six-ability stat-block table, the hero band shown when
   an entry has no uploaded art, and table styling for SRD prose
   (class tables, ancestry tables) which .note-body doesn't cover.

   Plain-CSS colours, so the light-mode contrast guard can't see them
   — every parchment value below is hand-matched to the remap table
   at the top of this file, the same deal as .wiki-link.
   ================================================================ */

/* Cross-links baked into body_html by main/srd/pipeline/linker.py. The
   .note-body selector out-ranks the generic ".note-body a" colour rule. */
.srd-link,
.note-body a.srd-link {
  color: #fbbf24; /* amber-400 — dark mode */
  text-decoration: underline;
  text-decoration-color: rgba(251, 191, 36, 0.35);
  text-underline-offset: 2px;
}
.srd-link:hover,
.note-body a.srd-link:hover {
  text-decoration-color: #fbbf24;
}
html:not(.dark) .srd-link,
html:not(.dark) .note-body a.srd-link {
  color: #7c2d12; /* amber-900 — parchment heading amber */
  text-decoration-color: rgba(146, 64, 14, 0.35);
}
html:not(.dark) .srd-link:hover,
html:not(.dark) .note-body a.srd-link:hover {
  text-decoration-color: #7c2d12;
}

/* The six abilities, two rows of three groups (STR DEX CON / INT WIS CHA) —
   the 2024 stat-block layout. Twelve narrow cells, so the type is small and
   the ability abbreviation carries the weight. */
.srd-abilities {
  width: 100%;
  border-collapse: collapse;
  font-size: 0.8125rem;
  font-variant-numeric: tabular-nums;
}
.srd-abilities th,
.srd-abilities td {
  padding: 0.25rem 0.5rem;
  text-align: center;
  border: 1px solid rgba(120, 113, 108, 0.35);
  color: #d6d3d1; /* stone-300 */
  white-space: nowrap;
}
.srd-abilities thead th {
  font-size: 0.625rem;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: #a8a29e; /* stone-400 */
}
.srd-abilities tbody th {
  font-family: Cinzel, serif;
  font-weight: 700;
  color: #fbbf24; /* amber-400 */
}
html:not(.dark) .srd-abilities th,
html:not(.dark) .srd-abilities td {
  border-color: rgba(196, 168, 130, 0.8);
  color: #44403c;
}
html:not(.dark) .srd-abilities thead th { color: #78716c; }
html:not(.dark) .srd-abilities tbody th { color: #7c2d12; }

/* Tables inside rendered SRD prose (class tables, draconic ancestry, magic-item
   variant tables). .note-body styles paragraphs and lists but not tables. */
.srd-prose table {
  width: 100%;
  border-collapse: collapse;
  margin: 0.75em 0;
  font-size: 0.875rem;
  display: block;
  overflow-x: auto;
}
.srd-prose th,
.srd-prose td {
  padding: 0.25rem 0.6rem;
  text-align: left;
  vertical-align: top;
  border: 1px solid rgba(120, 113, 108, 0.35);
}
.srd-prose thead th {
  color: #e7e5e4; /* stone-200 */
  font-weight: 600;
}
html:not(.dark) .srd-prose th,
html:not(.dark) .srd-prose td { border-color: rgba(196, 168, 130, 0.8); }
html:not(.dark) .srd-prose thead th { color: #292524; }

/* No-art hero band. The SRD licence covers text only, so most entries have no
   image — this is the deliberate fallback, not a placeholder for a missing
   asset: the entry's initial in Cinzel over a warm gradient. */
.srd-hero-fallback {
  display: flex;
  align-items: center;
  gap: 1rem;
  padding: 1.25rem 1.5rem;
  border-radius: 1rem;
  border: 1px solid rgba(120, 113, 108, 0.35);
  background: linear-gradient(135deg, rgba(69, 26, 3, 0.55), rgba(28, 25, 23, 0.55));
}
.srd-hero-initial {
  font-family: Cinzel, serif;
  font-size: 2.25rem;
  line-height: 1;
  font-weight: 700;
  color: #fbbf24; /* amber-400 */
}
.srd-hero-kind {
  font-size: 0.6875rem;
  text-transform: uppercase;
  letter-spacing: 0.12em;
  font-weight: 700;
  color: #a8a29e; /* stone-400 */
}
html:not(.dark) .srd-hero-fallback {
  border-color: rgba(196, 168, 130, 0.8);
  background: linear-gradient(135deg, rgba(217, 119, 6, 0.14), rgba(212, 184, 150, 0.35));
}
html:not(.dark) .srd-hero-initial { color: #7c2d12; }
html:not(.dark) .srd-hero-kind { color: #78716c; }

/* Rules-chapter contents panel (entry_rules.html). A folded CARD on a phone,
   a bare sticky rail from lg up. Plain CSS rather than Tailwind utilities
   because `lg:bg-transparent` can't undo the parchment remap of
   `bg-stone-800/40` — `html:not(.dark) .bg-stone-800\/40 { ... !important }`
   outranks a one-class utility no matter how important it declares itself. */
.srd-toc-panel {
  border-radius: 0.75rem;
  border: 1px solid rgba(68, 64, 60, 0.4);
  background-color: rgba(41, 37, 36, 0.4);
}
html:not(.dark) .srd-toc-panel {
  border-color: rgba(196, 168, 130, 0.7);
  background-color: rgba(232, 213, 163, 0.4);
}
@media (min-width: 1024px) {
  .srd-toc-panel,
  html:not(.dark) .srd-toc-panel {
    border: 0;
    border-radius: 0;
    background-color: transparent;
  }
  /* Inside the pinned .srd-toc-rail the panel is the column that has to give:
     the "On this page" eyebrow stays put and .srd-toc-list scrolls under it. */
  .srd-toc-panel {
    display: flex;
    flex-direction: column;
    min-height: 0;
  }
}

/* Rules-chapter contents list (entry_rules.html). Just the hanging indent — the
   link colours are Tailwind utilities on the anchors themselves. */
.srd-toc li { line-height: 1.35; }

/* ---- Phase C: the compendium surfaces ---------------------------------
   Everything below is plain CSS, so the light-mode guard can't see it —
   every parchment value is hand-matched to the remap table at the top of
   this file (page #f5e6c8, card #e8d5a3, raised #d4b896, ink #3d2b1f,
   heading amber #7c2d12), the same deal as .srd-link above. */

/* The facts panel every entry page opens with (spell header, stat block,
   class core traits, item numbers). One surface, one radius, one amber
   hairline along the top so the eye lands on it before the prose. */
.srd-statcard {
  position: relative;
  overflow: hidden;
  padding: 1.25rem;
  border-radius: 1rem;
  border: 1px solid rgba(68, 64, 60, 0.6);
  background-color: #292524; /* stone-800 */
}
.srd-statcard::before {
  content: "";
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  height: 2px;
  background: linear-gradient(90deg, rgba(217, 119, 6, 0.75), rgba(217, 119, 6, 0));
}
html:not(.dark) .srd-statcard {
  border-color: rgba(196, 168, 130, 0.9);
  background-color: #e8d5a3;
}

/* A label/value pair grid (Casting Time, Rarity, Hit Die…). The rule down
   the left edge is what makes four facts read as a table rather than four
   loose paragraphs. */
.srd-factgrid > div {
  padding-left: 0.625rem;
  border-left: 2px solid rgba(180, 83, 9, 0.35);
}
html:not(.dark) .srd-factgrid > div { border-left-color: rgba(146, 64, 14, 0.3); }

/* Stat-block header lines: "AC 17", "HP 150 (20d10 + 40)". Bold Cinzel label
   run inline with its value, one faint rule per line — the 2024 book layout. */
.srd-lines > div {
  padding: 0.2rem 0;
  border-bottom: 1px solid rgba(120, 113, 108, 0.2);
}
.srd-lines > div:last-child { border-bottom: 0; }
.srd-lines dt {
  display: inline;
  font-family: Cinzel, serif;
  font-weight: 700;
  font-size: 0.8125rem;
  letter-spacing: 0.02em;
  color: #fbbf24; /* amber-400 */
}
.srd-lines dd { display: inline; margin-left: 0.35rem; }
html:not(.dark) .srd-lines > div { border-bottom-color: rgba(146, 64, 14, 0.18); }
html:not(.dark) .srd-lines dt { color: #7c2d12; }

/* The ability table sits between two amber rules, the way it does in print. */
.srd-statblock .srd-abilities-wrap {
  padding: 0.5rem 0;
  border-top: 2px solid rgba(180, 83, 9, 0.5);
  border-bottom: 2px solid rgba(180, 83, 9, 0.5);
}
html:not(.dark) .srd-statblock .srd-abilities-wrap {
  border-top-color: rgba(146, 64, 14, 0.45);
  border-bottom-color: rgba(146, 64, 14, 0.45);
}

/* Section rule inside an entry (Traits, Actions, Class Features, The Bard
   Table). Same eyebrow as DESIGN.md's section heading, plus the hairline
   that separates a stat block's blocks. */
.srd-section-heading {
  margin-bottom: 0.85rem;
  padding-bottom: 0.3rem;
  font-size: 0.6875rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.15em;
  color: rgba(245, 158, 11, 0.85); /* amber-500/85 */
  border-bottom: 1px solid rgba(180, 83, 9, 0.4);
}
html:not(.dark) .srd-section-heading {
  color: #7c2d12;
  border-bottom-color: rgba(146, 64, 14, 0.35);
}

/* Long-form rules prose (chapters, class features, spell descriptions) in
   EB Garamond — DESIGN.md's flavour face, and the SRD is the one place on the
   site with book-length text to set. UI chrome stays on Inter, and so do
   tables inside the prose: their figures have to line up. */
.srd-longform {
  font-family: "EB Garamond", Georgia, serif;
  font-size: 1.125rem;
  line-height: 1.7;
}
.srd-longform p { margin-bottom: 0.7em; }
.srd-longform li { margin-bottom: 0.3em; }
.srd-longform table { font-family: Inter, sans-serif; font-size: 0.875rem; }

.srd-leveltable { font-variant-numeric: tabular-nums; }

/* Sticky bars: the index filter rail and the glossary's A–Z strip.

   Three things a translucent, rounded, floating card got wrong, all visible
   while scrolling a 339-row index:
     * `top` was 6rem while base.html's sticky topbar is 4rem (+ the notch pad),
       so row cards paraded through the 2rem gap between the header's hairline
       and the bar;
     * the background was translucent stone-800 over a blur, so the cards were
       visible THROUGH it;
     * rounded corners and inset edges let a card's corner peek around it.
   So when it sticks it stops being a card: page-coloured, opaque, square, bled
   past the column edges, with one hairline underneath to divide it from the
   list. It stays a card on a phone, where it does not stick at all — a
   five-row filter bar pinned to a 375px viewport would eat the page.

   And a fourth thing, which is what `.is-stuck` / `.is-collapsed` below are for:
   a bar that stays FULL HEIGHT once it has stuck. Monsters is the worst case —
   the CR row wraps three or four lines — and a member's report was simply that
   the filters ate the screen they were trying to read. So once it has stuck it
   folds to one line (the summary row in srd/index.html) until its button is
   pressed. Read docs/srd-reference.md before changing any of this; the two
   invariants that are easy to break are (1) the bar's BOX never changes size,
   or every row below it jumps, and (2) the collapsed bar is not a surface —
   the rows scroll AND click straight through it. */
/* Below lg the filter bar is bare: its phone toggle is a full-width button in the
   "Browse all sections" style, and wrapping that in a card put one border inside
   another. The alpha bar keeps the card tier at every width. */
.srd-alphabar {
  border: 1px solid rgba(68, 64, 60, 0.6);
  border-radius: 0.75rem;
  padding: 0.75rem 1rem;
  background-color: #292524; /* stone-800 — the card tier */
}
html:not(.dark) .srd-alphabar {
  border-color: rgba(196, 168, 130, 0.9);
  background-color: #e8d5a3;
}

/* The stuck treatment. `--srd-stick-top` is base.html's topbar height exactly
   (h-[calc(4rem+var(--safe-top))], sticky at top-0), so the two edges meet with
   nothing between them. The negative margins bleed the bar past the row cards
   on both sides; the padding puts its contents back where they were.

   `--rail-top` is the same number plus the page's `py-8` — what a rail's old
   `lg:top-24` meant, except that this one honours the notch pad. It is the
   SITE-WIDE offset for a sticky rail (see `.page-rail` below); `--srd-rail-top`
   is kept as its alias so this section reads on its own terms. All three are
   the SINGLE SOURCE for their offset: nothing else in the section, and nothing
   in js/srd.js, may hold a copy (see .srd-sentinel below). */
:root {
  --srd-stick-top: calc(4rem + var(--safe-top, 0px));
  --rail-top: calc(var(--srd-stick-top) + 2rem);
  --srd-rail-top: var(--rail-top);
}

/* The general sticky rail (DESIGN.md "Feed + rail"): the right-hand column of a
   `grid lg:grid-cols-[minmax(0,1fr)_320px] … items-start` page.

   It goes on the GRID ITEM itself, never on a wrapper inside it. The grid's
   `items-start` makes the column content-height, so an inner div has no travel
   to stick within and the rule is silently inert — the bug the SRD rails hit
   first (see .srd-rail below). `align-self: start` is what `items-start`
   already gives us, restated so the class stands alone.

   No `max-height`/`overflow` on purpose: a few short cards do not need to
   scroll, and an `overflow: auto` ancestor would clip an absolutely-positioned
   popover inside the rail. A rail with a long list wants the .srd-rail
   treatment instead.

   Nothing below lg: on a phone the rail sits under the content (or does not
   render at all) and pinning it would be the bug, not the fix. */
@media (min-width: 1024px) {
  .page-rail {
    position: sticky;
    top: var(--rail-top);
    align-self: start;
  }
}

.srd-alphabar {
  position: sticky;
  top: var(--srd-stick-top);
  z-index: 20; /* under the topbar's z-[35] and the z-40 scrims — DESIGN.md */
  border: 0;
  border-bottom: 1px solid rgba(68, 64, 60, 0.6);
  border-radius: 0;
  margin-left: -0.75rem;
  margin-right: -0.75rem;
  padding: 0.75rem;
  background-color: #1c1917; /* stone-900 — the page ground */
}
html:not(.dark) .srd-alphabar {
  border-bottom-color: rgba(196, 168, 130, 0.9);
  background-color: #f5e6c8;
}

/* The 1px marker srdIndex watches to know the bar has stuck (js/srd.js).

   It is placed HERE, by `--srd-stick-top`, rather than by a number in the JS:
   one topbar-height above the bar's natural top, so it leaves the viewport at
   the exact instant the bar starts sticking. Its containing block is the
   `relative` srdIndex root it is the first child of. An IntersectionObserver on
   it costs two callbacks per journey down the page; the scroll listener it
   replaces would answer "not yet" sixty times a second. */
.srd-sentinel {
  position: absolute;
  top: calc(-1 * var(--srd-stick-top));
  left: 0;
  width: 1px;
  height: 1px;
  pointer-events: none;
}

/* The header row and the summary row that replaces it when the bar sticks are
   held to one minimum height, so swapping one for the other cannot shift the
   pill panel — or the 339 rows under it — by a pixel. */
.srd-filterbar-row { min-height: 1.5rem; }

@media (min-width: 1024px) {
  .srd-filterbar {
    position: sticky;
    top: var(--srd-stick-top);
    z-index: 20;
    max-height: 50vh;
    overflow-y: auto;
    border: 0;
    border-bottom: 1px solid rgba(68, 64, 60, 0.6);
    border-radius: 0;
    margin-left: -0.75rem;
    margin-right: -0.75rem;
    padding: 0.75rem;
    background-color: #1c1917;
    transition:
      background-color 150ms ease,
      border-color 150ms ease;
  }
  html:not(.dark) .srd-filterbar {
    border-bottom-color: rgba(196, 168, 130, 0.9);
    background-color: #f5e6c8;
  }

  /* STUCK: the summary line REPLACES the header row (rather than joining it) in
     both the collapsed and the expanded state, so the disclosure button stays
     put and keeps focus while the pills unfold under it. */
  .srd-filterbar.is-stuck .srd-filterbar-head { display: none; }
  .srd-filterbar.is-stuck .srd-filterbar-summary { display: flex; }

  /* COLLAPSED: the bar keeps its whole box — `visibility`, never `display`, so
     nothing below it moves — and stops being a surface. Transparent,
     click-through, and clipped so the hidden pills can't push out a scrollbar.
     `visibility: hidden` also takes the pills out of the tab order and the
     accessibility tree, which `opacity: 0` would not. */
  .srd-filterbar.is-collapsed,
  html:not(.dark) .srd-filterbar.is-collapsed {
    background-color: transparent;
    border-bottom-color: transparent;
    overflow: hidden;
    pointer-events: none;
  }
  .srd-filterbar.is-collapsed [data-srd-filters] { visibility: hidden; }
  .srd-filterbar.is-collapsed .srd-filterbar-summary { pointer-events: auto; }

  /* …which leaves the summary row to paint itself: the page colour with the
     same hairline the full bar has, bled back out over the bar's padding so the
     one line still runs edge to edge.

     `z-index: -1` is load-bearing. An absolutely positioned pseudo-element
     paints in a LATER step than its in-flow siblings, so without it the skirt
     would cover the very text it is a background for. Negative z-index drops it
     to the bottom of .srd-filterbar's stacking context (which its own
     `z-index: 20` creates) — behind the row's text, still over the rows
     scrolling past underneath. */
  .srd-filterbar.is-collapsed .srd-filterbar-summary::before {
    content: "";
    position: absolute;
    top: -0.75rem;
    left: -0.75rem;
    right: -0.75rem;
    bottom: -0.5rem;
    z-index: -1;
    background-color: #1c1917;
    border-bottom: 1px solid rgba(68, 64, 60, 0.6);
  }
  html:not(.dark) .srd-filterbar.is-collapsed .srd-filterbar-summary::before {
    background-color: #f5e6c8;
    border-bottom-color: rgba(196, 168, 130, 0.9);
  }
}

/* ---- The two rails that follow the viewport ---------------------------
   The sub-nav (srd/_subnav.html) and a rules chapter's contents list
   (srd/entry_rules.html). Same recipe as the map generator's .mg-rail, and the
   three load-bearing parts are the same three:

     sticky on the GRID ITEM   a sticky element travels only inside its
                               containing block. Both of these used to put
                               `lg:sticky lg:top-24` on an INNER div, whose
                               containing block is the wrapper around it and so
                               exactly its own height — zero travel, which is
                               why both simply scrolled away with the page.
     `align-self: start`       a grid item stretches to its row by default, and
                               a stretched sticky rail is as tall as the article
                               beside it, which puts its own scrollbar below the
                               fold.
     `min-height: 0`           without it a flex/grid item refuses to shrink
                               below its content and the height cap is ignored.

   `max-height`, not mapgen's `height`: these are content-tall lists, so the
   rail should end where the links do and only grow a scrollbar when the window
   is shorter than they are.

   Nothing here below lg. On a phone the rail is a fold-away panel above the
   article and pinning it would be the bug, not the fix. */
@media (min-width: 1024px) {
  .srd-rail,
  .srd-toc-rail {
    position: sticky;
    top: var(--srd-rail-top);
    align-self: start;
    min-height: 0;
    max-height: calc(100vh - var(--srd-rail-top) - 1rem);
    display: flex;
    flex-direction: column;
  }
  /* ONLY the link list scrolls. The sub-nav's search box sits outside it
     because its results dropdown is `absolute z-30`, and an ancestor with
     `overflow: auto` would clip the results to the width of the rail. */
  .srd-rail-body,
  .srd-toc-list {
    min-height: 0;
    overflow-y: auto;
  }
}

/* One scrollbar treatment for both rails, hand-matched to the parchment table
   above. (.mg-scroll is the same recipe but reads `--mg-*` variables that only
   exist on the map generator's island.) */
.srd-scroll {
  scrollbar-width: thin;
  scrollbar-color: rgba(120, 113, 108, 0.5) transparent;
}
.srd-scroll::-webkit-scrollbar { width: 8px; height: 8px; }
.srd-scroll::-webkit-scrollbar-track { background: transparent; }
.srd-scroll::-webkit-scrollbar-thumb {
  background: rgba(120, 113, 108, 0.5);
  border-radius: 9999px;
}
.srd-scroll::-webkit-scrollbar-thumb:hover { background: rgba(180, 83, 9, 0.6); }
html:not(.dark) .srd-scroll { scrollbar-color: rgba(196, 168, 130, 0.9) transparent; }
html:not(.dark) .srd-scroll::-webkit-scrollbar-thumb { background: rgba(196, 168, 130, 0.9); }
html:not(.dark) .srd-scroll::-webkit-scrollbar-thumb:hover { background: rgba(146, 64, 14, 0.5); }

/* Inline keycap in the landing page's "getting around" strip. Plain CSS
   because the string lives in a Jinja tuple, not as markup a designer edits. */
.srd-page kbd {
  font-family: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 0.75em;
  padding: 0.05em 0.4em;
  border-radius: 0.25rem;
  border: 1px solid rgba(120, 113, 108, 0.5);
  background-color: rgba(41, 37, 36, 0.8);
  color: #d6d3d1;
}
html:not(.dark) .srd-page kbd {
  border-color: rgba(146, 64, 14, 0.3);
  background-color: rgba(212, 184, 150, 0.5);
  color: #3d2b1f;
}

/* Both pledge bars — the session page's and the game night's — used to be
   fixed above the 61px bottom tab bar, and the footer needed extra padding or
   its links ended up underneath them. Neither is pinned any more: the phone's
   bottom edge belongs to the tab bar, and each page puts its Going / Maybe /
   Out in the reading order instead (the session's at the head of its rail, the
   night's at order-2, above Tonight's line-up). Both are ordinary cards in the
   flow now, so there is nothing for the footer to clear and the rule that did
   it is gone. Kept as a note so nobody re-adds the padding chasing a gap. */

/* The session page's action cluster (session_detail.html [data-action-cluster]):
   a two-column grid on a phone, where it can run to six controls — Edit, Cancel,
   Open chat, Share, Invite, Remind Me — of six different natural widths.

   The cells have to be FILLED, and the controls reach the grid through three
   different shapes: a bare <a>, a <form> wrapping its submit, and the share /
   remind macros' `inline-block` div (invite_modal already uses display:contents,
   so its button arrives as a direct child). A page template cannot reach the
   trigger inside a macro's wrapper, so it is done here — with CHILD combinators,
   which is the whole trick: the share and remind popovers and the invite
   dialog's own buttons sit deeper and are deliberately left alone. Widening
   those would stretch every item in an open dropdown.

   `white-space: normal` undoes .btn-secondary's own nowrap: at 390px a cell is
   about 155px, and "Record Attendance" does not fit one line inside it. Wrapping
   makes the row taller and its neighbour stretches to match, which is the
   alignment that was wanted. 639.98px = below Tailwind's `sm`, where the
   template switches back to the two-group flex row.

   One level deeper than looks right, and that is the point: `display: contents`
   flattens BOXES, not the DOM, and selectors match the DOM. The two groups are
   still element children of the cluster, so the controls are grandchildren —
   `[data-action-cluster] > form` matches nothing at all. Written at the wrong
   depth the rules silently did nothing and the grid came out ragged (154 / 113 /
   92 / 154 / 129 px), because grid stretched each ITEM while the trigger inside
   a form or an inline-block wrapper kept its natural width. */
@media (max-width: 639.98px) {
  [data-action-cluster] > div > a,
  [data-action-cluster] > div > button,
  [data-action-cluster] > div > form,
  [data-action-cluster] > div > div,
  [data-action-cluster] > div > form > button,
  [data-action-cluster] > div > div > button {
    width: 100%;
  }
  [data-action-cluster] > div > a,
  [data-action-cluster] > div > button,
  [data-action-cluster] > div > form > button,
  [data-action-cluster] > div > div > button {
    white-space: normal;
    text-align: center;
  }
}

/* Back-to-top button (_layout.html + srdBackToTop in js/srd.js).

   Plain CSS because the phone offset is arithmetic: base.html's bottom tab bar
   (nav[data-mobile-tabbar]) measures 61px of content and grows by --safe-bottom
   inside the installed app, so the button clears BOTH or it sits on the tabs.
   z-index 30 keeps it under the topbar's z-[35] and the z-40 scrims, per
   DESIGN.md's stacking rule — a floating control is not allowed to outrank the
   chrome. */
.srd-to-top {
  position: fixed;
  right: 0.75rem;
  bottom: calc(var(--safe-bottom, 0px) + 61px + 0.75rem);
  z-index: 30;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 2.5rem;
  height: 2.5rem;
  border-radius: 9999px;
  border: 1px solid rgba(68, 64, 60, 0.6);
  background-color: rgba(41, 37, 36, 0.8);
  color: #fbbf24;
  box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.35);
}
.srd-to-top:hover { color: #fcd34d; }
html:not(.dark) .srd-to-top {
  border-color: rgba(196, 168, 130, 0.9);
  background-color: rgba(232, 213, 163, 0.88);
  color: #7c2d12;
}
/* Desktop: no tab bar to clear, and the max-w-6xl content column ends well
   short of the corner, so the button can be bigger and fully opaque. */
@media (min-width: 1024px) {
  .srd-to-top {
    right: 1.5rem;
    bottom: 1.5rem;
    width: 2.75rem;
    height: 2.75rem;
    background-color: rgba(41, 37, 36, 0.92);
  }
  html:not(.dark) .srd-to-top { background-color: rgba(232, 213, 163, 0.95); }
}
/* Alpine's x-transition writes its duration inline, so the reduced-motion
   opt-out has to out-shout it. The filter bar joins it: the surface fading in
   and out as the bar sticks and un-sticks is decoration, and someone who has
   asked for less motion should get the flip, not the fade. */
@media (prefers-reduced-motion: reduce) {
  .srd-to-top,
  .srd-filterbar { transition-duration: 0ms !important; }
}

/* Landing tiles' glyph chip. */
.srd-tile-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2.25rem;
  height: 2.25rem;
  border-radius: 0.625rem;
  color: #fbbf24;
  border: 1px solid rgba(180, 83, 9, 0.3);
  background-color: rgba(180, 83, 9, 0.15);
}
html:not(.dark) .srd-tile-icon {
  color: #7c2d12;
  border-color: rgba(146, 64, 14, 0.3);
  background-color: rgba(217, 119, 6, 0.14);
}

/* Cross-link hover card (js/srd.js `srdPeek`). Its surface is Tailwind on the
   element; this is only the preview HTML inside it — including neutralising
   the nested .srd-links a preview inevitably contains, which are not tap
   targets at that size. */
.srd-peek-body p { margin-bottom: 0.35em; }
.srd-peek-body p:last-child { margin-bottom: 0; }
.srd-peek-body a,
.srd-peek-body a.srd-link {
  color: inherit;
  text-decoration: none;
  pointer-events: none;
}

/* ================================================================
   PRINT — the SRD is a reference, and references get printed and
   passed round a table. Strip base.html's chrome (sidebar, topbar,
   mobile tab bar, More sheet, footer, grain) and anything the SRD
   templates marked .no-print, then set the article black-on-white.

   The CC BY 4.0 notice ([data-srd-notice]) is deliberately NOT
   hidden: the licence requires the attribution to travel with the
   text, and a printout is a copy of the text.
   ================================================================ */
@media print {
  @page { margin: 1.5cm; }

  aside.fixed.inset-y-0,
  header.sticky.top-0,
  [data-mobile-tabbar],
  [x-show="moreOpen"],
  .grain,
  footer,
  .no-print { display: none !important; }

  html, body { background: #fff !important; }

  .srd-page,
  .srd-page * {
    background: transparent !important;
    color: #000 !important;
    box-shadow: none !important;
    text-shadow: none !important;
  }
  .srd-page { max-width: none; padding: 0; }
  .srd-print { font-size: 11pt; }
  .srd-page a { text-decoration: underline; }
  .srd-statcard { border-color: #999 !important; }
  .srd-statcard::before { display: none; }
  .srd-hero-fallback,
  .srd-hero-initial,
  .srd-hero-kind { display: none !important; }
  .srd-abilities th,
  .srd-abilities td,
  .srd-prose th,
  .srd-prose td,
  .srd-section-heading,
  .srd-lines > div { border-color: #999 !important; }
  .srd-statblock .srd-abilities-wrap { border-color: #666 !important; }
  /* Keep a stat block, a trait, or a fact panel from splitting across pages;
     a whole chapter deliberately may. */
  .srd-statcard,
  .srd-trait,
  .srd-abilities { break-inside: avoid; page-break-inside: avoid; }
  [data-srd-notice] { font-size: 8pt; margin-top: 1.5rem; }
}

/* ================================================================
   MAP GENERATOR (F-206)
   The /map-generator tool: a three-pane editor island (settings
   rail, canvas viewport, key sheet) plus its modals.

   ISLAND RULES (DESIGN.md "Bespoke islands"). The page HEADER is
   on-system — eyebrow, serif amber h1, the stacking action row —
   and only the body below it is bespoke. The surfaces here sit on
   the standard tiers (card = stone-800, inset = stone-900/50,
   raised = stone-700) and the accent is amber, so the island reads
   as part of the app rather than as a different product.

   THE CANVAS IS ALWAYS PARCHMENT, in both site themes, the way the
   Mausoleum is always night. The map is a drawing on paper: a map
   re-tinted for tavern mode would not match its own printout, and
   the printout is the point. Only the chrome around it follows the
   theme.

   Plain-CSS colours, so the light-mode contrast guard cannot see
   them — every parchment value below is hand-matched to the remap
   table at the top of this file, the same deal as .srd-link and
   .wiki-link. They are collected as --mg-* custom properties so the
   parchment override is one block of values rather than forty
   duplicated rules.
   ================================================================ */

[data-mapgen] {
  --mg-card: #292524;                /* stone-800 */
  --mg-inset: rgba(28, 25, 23, 0.5); /* stone-900/50 */
  --mg-raised: #44403c;              /* stone-700 */
  --mg-border: rgba(68, 64, 60, 0.6);
  --mg-border-strong: #57534e;       /* stone-600 */
  --mg-text: #e7e5e4;                /* stone-200 */
  --mg-muted: #a8a29e;               /* stone-400 */
  --mg-faint: #78716c;               /* stone-500 */
  --mg-accent: #fbbf24;              /* amber-400 */
  --mg-accent-soft: rgba(217, 119, 6, 0.2);
  --mg-accent-border: rgba(217, 119, 6, 0.3);
  --mg-danger: #f87171;              /* red-400 */
  /* The paper the canvas sits on. Matches render/theme.js PARCHMENT.paper — if these
     two ever disagree the map appears to float on a differently-coloured page. */
  --mg-paper: #e9dcbb;
  --mg-paper-edge: rgba(58, 50, 38, 0.35);
}

html:not(.dark) [data-mapgen] {
  --mg-card: #e8d5a3;                /* parchment card */
  --mg-inset: rgba(212, 184, 150, 0.45);
  --mg-raised: #d4b896;              /* parchment raised */
  --mg-border: rgba(146, 116, 74, 0.45);
  --mg-border-strong: #a1804f;
  --mg-text: #3d2b1f;                /* parchment ink */
  --mg-muted: #6b5137;
  --mg-faint: #8a7154;
  --mg-accent: #7c2d12;              /* parchment heading amber */
  --mg-accent-soft: rgba(146, 64, 14, 0.14);
  --mg-accent-border: rgba(146, 64, 14, 0.35);
  --mg-danger: #b91c1c;
  --mg-paper-edge: rgba(58, 50, 38, 0.25);
}

/* ---- Layout ---------------------------------------------------- */

/* THE PAGE IS FULL BLEED, which DESIGN.md's island rule 1 allows a full-bleed editor to do.
   It replaces the site's usual `max-w-6xl` container for this page only; the header above it
   (eyebrow, h1, actions) stays on-system, as rule 4 requires.

   Why: inside a 72rem container on a 1440p monitor the map column was about half the window
   with the two rails taking the rest, and a third of the screen either side was empty page.
   The map is the whole point of the tool, so a wider monitor should buy map, not margin. The
   rails keep FIXED widths and the map column takes everything left over — that is the whole
   trick, and it is why the columns below are `20rem minmax(0, 1fr) 18rem` rather than
   fractions. */
.mg-page {
  width: 100%;
  padding: 2rem 1rem;
  /* THE BREAKPOINTS BELOW ARE CONTAINER QUERIES, not media queries, and that is the whole
     reason this works. The site sidebar takes about 288px of a desktop window, so a viewport
     query is asking the wrong question: at 1280px wide the island is 960px, and a three-pane
     layout that looks right on a 1280px MOCKUP leaves 320px of map on the real page. Asking
     the island how wide IT is gets the same answer whether the sidebar is open, collapsed, or
     absent on a future page that embeds this editor. */
  container-type: inline-size;
  container-name: mg;
}
@media (min-width: 640px) {
  .mg-page { padding: 2rem 1.5rem; }
}
@media (min-width: 1280px) {
  .mg-page { padding: 2rem 2rem; }
}
@media (min-width: 1920px) {
  /* A wider gutter only once there is width to spare — at 1280 every pixel of it is map. */
  .mg-page { padding: 2rem 3rem; }
}

.mg-shell {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: 1rem;
}
/* The map first on a phone: the settings rail collapses to a button and the key sheet
   follows the map, because on a 375px screen the map is the only thing worth the fold. */

/* THE PANE THRESHOLDS ARE IN PIXELS AND THEY ARE MIRRORED IN ui/panes.js. Keep the two in
   step — panes.js is what the JS measurement and the node test read, and this block is what
   actually draws it. They were `rem` and that is exactly how the bug got in: read on.

   THE BUG THIS FIXES. Three panes used to start at 80rem = 1280px OF ISLAND, and the number
   was derived from a 1920px monitor at 100% browser scale. Windows ships a 1920×1200 panel at
   125% display scaling, which is a 1536px CSS viewport: take off the 256px site sidebar and
   the 2rem gutters and the island is 1216px — 64px short. So Joe's desktop fell into the
   two-pane branch, where the key sheet spanned `1 / -1` UNDER the sticky settings rail, at
   full island width, with a 32rem inner scroller. Its heading sat behind the rail, the page
   grew a scrollbar it did not need, and scrolling it dragged the un-sticky map up under the
   sticky topbar. Every symptom in the report is that one threshold.

   So: three panes from 1180px of island, which is the width at which the map column can still
   keep 55% of the island with both rails beside it, and the rails are `clamp()`ed against
   container-query units so they give width back to the map as the island narrows instead of
   stepping. Two panes from 860px. One column below that. */
@container mg (min-width: 860px) {
  .mg-shell {
    /* 17rem floor / 20rem ceiling: at 860 the rail is 272px and the map still has two
       thirds of the island. */
    grid-template-columns: clamp(17rem, 26cqw, 20rem) minmax(0, 1fr);
    align-items: start;
  }
  /* THE RIGHT-HAND COLUMN GOES UNDER THE MAP, IN THE MAP'S COLUMN — not `1 / -1`. Spanning
     the full island put it under the sticky settings rail, which then floated over its
     heading the moment the page moved. In the map column it is a normal block with a natural
     height and its own bounded lists, and nothing can overlap it. */
  .mg-side {
    grid-column: 2;
    position: static;
    height: auto;
  }
}

/* THREE PANES. 1180px of island is the arithmetic: the rails floor at 17rem + 13.5rem = 488px
   and the gaps take 32, so the map keeps 660px — 56% of the island, and it only goes up from
   there. Past ~1520 the rails reach their 20rem/18rem ceilings and every further pixel is
   map, which is the whole point of the full-bleed page. */
@container mg (min-width: 1180px) {
  .mg-shell { grid-template-columns: clamp(17rem, 21cqw, 20rem) minmax(0, 1fr) clamp(13.5rem, 17.5cqw, 18rem); }
  .mg-side { grid-column: auto; }
}

/* The right-hand column is TWO CARDS, stacked — the Key and the Objects panel — inside one
   grid item. One item rather than two because the grid's third column is a POSITION, and
   because the sticky recipe below then has exactly one owner. */
.mg-side {
  display: flex;
  flex-direction: column;
  gap: 1rem;
  min-width: 0;
}

.mg-rail,
.mg-key,
.mg-objects {
  background: var(--mg-card);
  border: 1px solid var(--mg-border);
  border-radius: 1rem;
  padding: 1rem;
}
.mg-rail-toggle {
  display: flex;
  width: 100%;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
  font-size: 0.875rem;
  font-weight: 600;
  color: var(--mg-accent);
  font-family: "Cinzel", serif;
}
@container mg (min-width: 860px) {
  /* On desktop the rail is always open, so its toggle is noise. */
  .mg-rail-toggle { display: none; }
  .mg-rail-body { display: block !important; }

  /* THE SIDE PANELS SCROLL, THE MAP DOES NOT. On a 1080p monitor the settings column was
     taller than the window, so reaching the lower sliders scrolled the whole page and took
     the map — the thing the sliders change — off the screen with it. Both rails are sticky
     within the viewport with their own scrollbar, so the map is always visible beside them.
     (The sections collapse as well, see .mg-group-head: that is the other half of the same
     fix, because a type with a dozen sliders is still tall with everything open.)

     THREE THINGS ARE LOAD-BEARING HERE and the panel overflows the window without any one:

       `align-self: start`   a grid item stretches to its row by default, and a stretched
                             sticky panel is as tall as the MAP is, which is what put its
                             own scrollbar below the fold.
       `--mg-rail-offset`    where the panel sits when the page has not been scrolled. The
                             sticky `top` is where it ends up AFTER scrolling; sizing to that
                             makes it overflow before you have scrolled, which is exactly
                             when someone first reaches for it. ui/app.js measures the real
                             number and writes it here; the fallback below is a safe
                             over-estimate (site topbar + the page header at desktop).
       `min-height: 0`       without it a grid item refuses to shrink below its content and
                             `max-height` is quietly ignored. */
  .mg-rail {
    position: sticky;
    top: 6rem;
    align-self: start;
    min-height: 0;
    /* HEIGHT, not max-height, and the same one for both: the three columns are meant to end
       on one line. With `max-height` the key panel stopped wherever its last entry did — so
       a map with six areas left the right-hand column floating half way up beside a
       full-height settings rail, which reads as a layout bug rather than as a short list. */
    height: calc(100vh - var(--mg-rail-offset, 15rem));
    overflow-y: auto;
  }
}

/* The right-hand column gets the viewport-height treatment ONLY in three-pane mode, where it
   is a column of its own beside the map. In two-pane it is a plain block in the map's column
   with bounded lists — a sticky, window-tall panel there would be pinned over the very map it
   is a key to, which is precisely what the report was looking at.

   THE COLUMN SCROLLS ITS TWO CARDS, NOT ITSELF. The Key takes what it needs up to 60% of the
   height and scrolls inside that; the Objects panel takes the rest and scrolls its grid. A
   single scroller over both would mean scrolling past thirty numbered rooms to reach the
   furniture, which is the state the Key's collapse exists to avoid — and a column that grew
   with its content would put the grid below the fold on every dungeon.

   THE KEY COMES FIRST, AND THE FLEX BASES SAY SO. The first cut of this gave the Key
   `flex: 0 1 auto` and the Objects panel `flex: 1 1 auto`, which reads as "the Key up to its
   cap, the grid the rest" and does the opposite: when two flex items overflow their column,
   each gives up height IN PROPORTION TO ITS BASIS, and the grid's basis is every tile it
   owns — two thousand pixels against a four-room Key's two hundred. The Key was shrunk to a
   single row on every dungeon (Joe: "the space allotted to the Key list is too short by
   far"). So the Key now never shrinks at all (`flex-shrink: 0`, capped by its max-height) and
   the grid's basis is ZERO: it takes whatever is left, and its min-height is the floor that
   keeps the picker a picker — search box plus two rows of tiles — on a short window. */
@container mg (min-width: 1180px) {
  .mg-side {
    position: sticky;
    top: 6rem;
    align-self: start;
    min-height: 0;
    height: calc(100vh - var(--mg-rail-offset, 15rem));
  }
  .mg-key {
    flex: 0 0 auto;
    min-height: 0;
    /* FOUR ROWS AT MOST. A key row (name, two-line description) is about 5.6rem and the
       heading and checkbox above it about 4.5rem, so 28rem shows four rows and the top of a
       fifth — enough to see that the list goes on. On a tall screen the percentage alone let
       seven rows through, and Joe's call was that the Objects grid is the thing to see more
       of: "at most 4 keys, that leaves more room for objects". The percentage still holds
       on a short window, where 28rem would leave the picker its 15rem floor and nothing more. */
    max-height: min(60%, 28rem);
    overflow-y: auto;
  }
  .mg-objects {
    flex: 1 1 0;
    min-height: 15rem;
  }
}

/* One scrollbar treatment for every scrolling surface in the island — the settings rail,
   the key list and the palette strip — so they read as one panel system rather than as
   three widgets that happen to be next to each other. Firefox takes the two-value shorthand;
   WebKit and Blink need the pseudo-elements, and neither inherits the other's. */
.mg-scroll {
  scrollbar-width: thin;
  scrollbar-color: var(--mg-border-strong) transparent;
}
.mg-scroll::-webkit-scrollbar { width: 8px; height: 8px; }
.mg-scroll::-webkit-scrollbar-track { background: transparent; }
.mg-scroll::-webkit-scrollbar-thumb {
  background: var(--mg-border-strong);
  border-radius: 9999px;
}
.mg-scroll::-webkit-scrollbar-thumb:hover { background: var(--mg-accent-border); }
.mg-rail-body { margin-top: 0.75rem; }
/* A CONTAINER query, not a media query, and for the same reason as the panes above: the gap
   under the toggle only exists while the toggle does, and the toggle goes away when the
   ISLAND is wide enough, not when the window is. (A 1024px window with the site sidebar open
   is a 736px island — one column, toggle still there.) */
@container mg (min-width: 860px) {
  .mg-rail-body { margin-top: 0; }
}

.mg-group { margin-bottom: 1.25rem; }
.mg-group:last-child { margin-bottom: 0; }
.mg-group-title {
  font-family: "Cinzel", serif;
  font-size: 0.75rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  color: var(--mg-accent);
  opacity: 0.75;
  margin-bottom: 0.6rem;
  display: block;
}
/* The rail's sections collapse (DESIGN.md's disclosure pattern: eyebrow heading turned into
   a button, chevron on the right). The heading keeps its own styling so collapsing a section
   does not restyle it — the button is a wrapper, not a new look. */
.mg-group-head {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  width: 100%;
  text-align: left;
}
.mg-group-head .mg-group-title { margin-bottom: 0; }
.mg-group-head:hover .mg-group-title { opacity: 1; }
.mg-chevron {
  width: 1rem;
  height: 1rem;
  margin-left: auto;
  flex: none;
  color: var(--mg-faint);
  transition: transform 0.15s ease;
}
.mg-group-head:hover .mg-chevron { color: var(--mg-accent); }
.mg-group-head + [x-show] { margin-top: 0.6rem; }

/* ---- Type tiles ------------------------------------------------- */
.mg-types {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 0.5rem;
}
.mg-type {
  text-align: left;
  padding: 0.55rem 0.6rem;
  border-radius: 0.75rem;
  border: 1px solid var(--mg-border);
  background: var(--mg-inset);
  transition: border-color 0.15s ease;
}
.mg-type:hover { border-color: var(--mg-accent-border); }
.mg-type.is-on {
  border-color: var(--mg-accent-border);
  background: var(--mg-accent-soft);
}
.mg-type-name {
  display: block;
  font-size: 0.8rem;
  font-weight: 700;
  color: var(--mg-text);
}
.mg-type.is-on .mg-type-name { color: var(--mg-accent); }
.mg-type-blurb {
  display: block;
  font-size: 0.68rem;
  line-height: 1.25;
  color: var(--mg-faint);
  margin-top: 0.15rem;
}

/* ---- Fields ----------------------------------------------------- */
.mg-label {
  display: block;
  font-size: 0.75rem;
  font-weight: 500;
  color: var(--mg-muted);
  margin-bottom: 0.3rem;
}
.mg-field {
  width: 100%;
  padding: 0.4rem 0.6rem;
  border-radius: 0.5rem;
  border: 1px solid var(--mg-border-strong);
  background: var(--mg-inset);
  color: var(--mg-text);
  font-size: 0.82rem;
  margin-bottom: 0.6rem;
}
.mg-field:focus {
  outline: none;
  border-color: transparent;
  box-shadow: 0 0 0 2px var(--mg-accent);
}
.mg-pair {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 0.5rem;
}
.mg-help {
  font-size: 0.68rem;
  color: var(--mg-faint);
  margin-top: -0.25rem;
  margin-bottom: 0.4rem;
}
.mg-seed-row {
  display: flex;
  gap: 0.4rem;
  align-items: flex-start;
}
.mg-seed-row .mg-field { margin-bottom: 0.4rem; }
.mg-icon-btn {
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2.05rem;
  height: 2.05rem;
  border-radius: 0.5rem;
  border: 1px solid var(--mg-border-strong);
  background: var(--mg-raised);
  color: var(--mg-text);
}
.mg-icon-btn:hover:not([disabled]) { color: var(--mg-accent); border-color: var(--mg-accent-border); }
/* A locked layer's re-roll button. Dimmed rather than hidden: the row still has to explain
   why nothing happens, and a button that vanishes reads as a rendering bug. */
.mg-icon-btn[disabled] { opacity: 0.35; cursor: default; }
.mg-icon-btn-sm { width: 1.6rem; height: 1.6rem; }

/* ---- Parameter sliders ------------------------------------------ */
.mg-param { margin-bottom: 0.75rem; }
.mg-param-head { display: flex; align-items: baseline; justify-content: space-between; }
.mg-param-value { font-size: 0.7rem; color: var(--mg-accent); font-variant-numeric: tabular-nums; }
.mg-slider { width: 100%; accent-color: var(--mg-accent); }
.mg-param-ends { font-size: 0.65rem; color: var(--mg-faint); }
.mg-param-note { margin: 0.35rem 0 0; font-size: 0.72rem; font-style: italic; line-height: 1.35; color: var(--mg-faint); }
.mg-check {
  display: flex;
  align-items: center;
  gap: 0.45rem;
  font-size: 0.78rem;
  color: var(--mg-muted);
  margin-bottom: 0.4rem;
}
.mg-multi { display: flex; flex-wrap: wrap; gap: 0.3rem; }
.mg-chip {
  padding: 0.2rem 0.5rem;
  font-size: 0.65rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  border-radius: 9999px;
  border: 1px solid var(--mg-border);
  background: var(--mg-inset);
  color: var(--mg-muted);
}
.mg-chip.is-on {
  background: var(--mg-accent-soft);
  border-color: var(--mg-accent-border);
  color: var(--mg-accent);
}

/* ---- Layers ------------------------------------------------------ */
.mg-layers { display: flex; flex-direction: column; gap: 0.2rem; }
.mg-layer {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  padding: 0.15rem 0.2rem;
  border-radius: 0.4rem;
}
.mg-layer:hover { background: var(--mg-inset); }
.mg-layer-name { flex: 1 1 auto; font-size: 0.78rem; color: var(--mg-text); min-width: 0; }
.mg-layer-n { font-size: 0.65rem; color: var(--mg-faint); font-variant-numeric: tabular-nums; }
.mg-lock {
  flex: none;
  display: inline-flex;
  width: 1.6rem;
  height: 1.6rem;
  align-items: center;
  justify-content: center;
  border-radius: 0.4rem;
  color: var(--mg-faint);
}
.mg-lock.is-on { color: var(--mg-accent); background: var(--mg-accent-soft); }

/* ---- The viewport ------------------------------------------------ */
.mg-stage { min-width: 0; }
.mg-viewport {
  position: relative;
  /* Traps the island's z-ladder inside the panel — DESIGN.md's stacking rule, the same
     treatment .leaflet-container gets. Nothing in here can outbid the topbar. */
  isolation: isolate;
  /* Every touch belongs to pointer.js: without this the browser claims a one-finger drag
     for page scrolling before the second finger of a pinch ever arrives. */
  touch-action: none;
  overflow: hidden;
  border-radius: 0.75rem;
  border: 1px solid var(--mg-border);
  background: var(--mg-paper);
  box-shadow: inset 0 0 0 1px var(--mg-paper-edge);
  height: 58vh;
  min-height: 20rem;
  cursor: crosshair;
}
/* A CONTAINER query again: the canvas is height-capped in the layouts where the rail is
   BESIDE it (two and three pane), because those are the ones meant to fit the window in one
   piece. In the one-column layout the map is stacked with everything else and a window-tall
   canvas would push the tools off the fold — which is the same complaint, upside down. */
@container mg (min-width: 860px) {
  /* THE WHOLE ISLAND FITS THE WINDOW. The canvas took a fixed share of the viewport height
     (70vh), which meant the tool strip UNDER it was pushed below the fold on a 1080p screen
     once the browser's own chrome and the taskbar had taken their cut — you could see the
     map and not reach the tools. So the canvas takes what is LEFT: the window, less where
     the island starts, less everything in this column below the canvas (floor tabs, tool
     strip, palette strip, status line), less a little air. `--mg-stage-offset` is that sum,
     measured by ui/app.js — `calc()` cannot see where an element sits or how tall its
     siblings are — and re-measured whenever the strip below changes height, which it does
     every time a tool with a palette is picked.

     The floor is 360 px: below that the map is not worth looking at, and a window too short
     to hold the island is better scrolled than shrunk into uselessness. */
  .mg-viewport {
    height: calc(100vh - var(--mg-stage-offset, 30rem));
    min-height: 22.5rem;
  }
}
.mg-canvas {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
}
.mg-canvas-overlay { pointer-events: none; }
.mg-busy {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgba(28, 25, 23, 0.55);
}
.mg-busy-text { font-size: 0.8rem; color: #e7e5e4; }

/* ---- The loading card -------------------------------------------------
   What stands where the editor will be, from the browser's FIRST paint until the map
   generator's modules have arrived and mounted the island.

   IT IS DRIVEN BY ONE ATTRIBUTE AND NOTHING ELSE. The island root carries `x-ignore` in the
   template; js/mapgen/index.js removes it the moment it registers the component and calls
   `Alpine.initTree`. So "the island has not mounted" is a CSS-visible fact, and the card
   needs no Alpine directive of its own — which is the whole point, because a card that
   needed Alpine could not be painted before Alpine had started. (`.mg-shell` keeps its
   `x-cloak`: that is the other half — the editor stays hidden until it has its data.)

   ITS HEIGHT MATCHES THE ISLAND'S so the swap is not a jump. In three panes the rails are
   `100vh - var(--mg-rail-offset)` tall and this uses the same sum with the same fallback
   (ui/app.js has not measured yet at this point, by definition, so it is always the
   fallback that is used here — a safe over-estimate). Below that the layout is not
   window-tall, so a fixed 18rem is closer than a viewport sum would be. */
.mg-loading {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  text-align: center;
  gap: 0.35rem;
  min-height: 18rem;
  padding: 2rem 1rem;
  border: 1px solid var(--mg-border);
  border-radius: 1rem;
  background: var(--mg-inset);
}
/* Shown ONLY while the root is still ignored — i.e. only while the island is not mounted. */
.mg-page:not([x-ignore]) .mg-loading { display: none; }
.mg-loading-spinner {
  width: 2rem;
  height: 2rem;
  margin-bottom: 0.5rem;
  color: var(--mg-accent);
  /* Its own keyframes rather than Tailwind's `animate-spin`: this file is hand-written CSS
     in the mg- namespace (DESIGN.md — the compiled build is a separate artefact), and a
     spinner that stops spinning the day a utility drops out of the build is a spinner that
     reads as a hung page. */
  animation: mg-spin 1s linear infinite;
}
@keyframes mg-spin {
  to { transform: rotate(360deg); }
}
.mg-loading-msg {
  font-family: "Cinzel", serif;
  letter-spacing: 0.02em;
  color: var(--mg-accent);
}
.mg-loading-sub {
  font-size: 0.8rem;
  color: var(--mg-muted);
  max-width: 26rem;
}
@media (prefers-reduced-motion: reduce) {
  .mg-loading-spinner { animation: none; }
}
@container mg (min-width: 1180px) {
  .mg-loading { min-height: calc(100vh - var(--mg-rail-offset, 15rem)); }
}

/* ---- Floor tabs and the tool strip ------------------------------- */
.mg-floors { display: flex; gap: 0.3rem; margin-top: 0.5rem; }
.mg-floor {
  padding: 0.25rem 0.7rem;
  font-size: 0.72rem;
  font-weight: 700;
  border-radius: 0.5rem 0.5rem 0 0;
  border: 1px solid var(--mg-border);
  border-bottom: none;
  background: var(--mg-inset);
  color: var(--mg-muted);
}
.mg-floor.is-on { background: var(--mg-accent-soft); color: var(--mg-accent); border-color: var(--mg-accent-border); }
/* "+ Storey" and "Cellar" sit at the far end of the tab row, so they read as acting on the
   set of tabs rather than as another tab. */
.mg-floor-actions { display: flex; gap: 0.3rem; margin-left: auto; }
.mg-floor-act {
  padding: 0.25rem 0.6rem;
  font-size: 0.7rem;
  font-weight: 700;
  border-radius: 0.5rem;
  border: 1px solid var(--mg-border);
  background: var(--mg-inset);
  color: var(--mg-muted);
}
.mg-floor-act:hover { color: var(--mg-accent); border-color: var(--mg-accent-border); }
.mg-floor-act.is-on { color: var(--mg-accent); background: var(--mg-accent-soft); border-color: var(--mg-accent-border); }

/* ---- The palette strip ------------------------------------------------- */
/* One strip for every tool that is a CHOICE of something (ui/palette.js). It WRAPS on a
   desktop — forty-six props in one scrolling row would be a lucky dip — and scrolls
   sideways on a phone, where a wrapped grid of swatches would push the map off the fold. */
.mg-palette {
  display: flex;
  gap: 0.25rem;
  margin-top: 0.4rem;
  padding: 0.35rem;
  border-radius: 0.6rem;
  border: 1px solid var(--mg-border);
  background: var(--mg-card);
  flex-wrap: nowrap;
  overflow-x: auto;
  overflow-y: hidden;
  scrollbar-width: thin;
}
@media (min-width: 640px) {
  .mg-palette {
    flex-wrap: wrap;
    overflow-x: hidden;
    overflow-y: auto;
    max-height: 9.5rem;
  }
}
.mg-swatch-btn {
  flex: none;
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  padding: 0.2rem;
  border-radius: 0.45rem;
  border: 1px solid transparent;
  color: var(--mg-muted);
  font-size: 0.7rem;
  line-height: 1.1;
}
.mg-swatch-btn:hover { border-color: var(--mg-accent-border); color: var(--mg-accent); }
.mg-swatch-btn.is-on { border-color: var(--mg-accent); background: var(--mg-accent-soft); color: var(--mg-accent); }
/* 34px is SWATCH_PX in ui/palette.js; the canvas is drawn at device resolution and sized
   back down here, so a stamp swatch is crisp on a phone. */
.mg-swatch {
  display: block;
  position: relative;
  width: 34px;
  height: 34px;
  border-radius: 0.3rem;
  overflow: hidden;
}
.mg-swatch-chip { background: var(--mg-paper); box-shadow: inset 0 0 0 1px var(--mg-paper-edge); }
.mg-swatch-wash { position: absolute; inset: 0; }
.mg-swatch-label { padding: 0 0.3rem 0 0.1rem; white-space: nowrap; font-weight: 600; }

.mg-tools {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.3rem;
  margin-top: 0.5rem;
  padding: 0.35rem;
  border-radius: 0.6rem;
  border: 1px solid var(--mg-border);
  background: var(--mg-card);
}
.mg-tools-spacer { flex: 1 1 auto; }
.mg-tool {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2.1rem;
  height: 2.1rem;
  border-radius: 0.5rem;
  color: var(--mg-muted);
}
.mg-tool:hover:not([disabled]) { color: var(--mg-accent); background: var(--mg-inset); }
.mg-tool.is-on { color: var(--mg-accent); background: var(--mg-accent-soft); }
.mg-tool[disabled] { opacity: 0.35; cursor: default; }
/* THE SAME IDIOM FOR THE MODALS' OWN BUTTONS. `.btn-primary` is a site-wide class with no
   disabled state of its own, so Export's "Download" while a job is running and Import's
   "Open this map" before a file has been read both looked pressable and did nothing. A
   button that looks enabled and answers a click with silence is exactly the failure the
   import dialog was built to stop, so it must not be the dialog's own last word. Scoped to
   the island, because changing `.btn-primary` everywhere is a whole-site decision. */
[data-mapgen] .btn-primary[disabled],
[data-mapgen] .btn-secondary[disabled] { opacity: 0.45; cursor: default; }
/* The one strip button that carries a word as well as a glyph ("Up to the first floor"). It
   grows to fit rather than truncating, because the whole point of it is that you can read
   where the stairs go without pressing anything. */
.mg-tool-wide { width: auto; gap: 0.3rem; padding: 0 0.45rem; }
.mg-tool-label { font-size: 0.68rem; white-space: nowrap; }
@media (max-width: 640px) {
  /* On a phone the strip is already three rows; the word goes and the tooltip carries it. */
  .mg-tool-label { display: none; }
  .mg-tool-wide { width: 2.1rem; padding: 0; }
  /* EXCEPT what you are holding, which is the one word that has to be readable while the
     other hand is on the map: the Objects panel is a whole screen away at this width, so a
     chip reduced to a glyph would leave nothing on screen saying what the next tap places. */
  [data-mapgen-placing] { width: auto; padding: 0 0.45rem; }
  [data-mapgen-placing] .mg-tool-label { display: inline; }
}

/* ---- "More tools": every other map type's tools ---------------------
   A SECOND ROW UNDER THE STRIP, closed by default (F-206, round 4, 4b). The disclosure
   button is always in the flow, so the only thing that ever changes this block's height is
   the member pressing it — the same rule the turn, duplicate and "Placing" buttons follow,
   and for the same reason: a control that jiggles the map under your hand is worse than one
   that is greyed out.

   The row shares `.mg-tool` with the strip above it, so a foreign tool is the same button
   with the same states; only the grouping and the group's name are new. */
.mg-tools-more { margin-top: 0.3rem; }
.mg-more-toggle {
  display: inline-flex;
  align-items: center;
  gap: 0.25rem;
  padding: 0.2rem 0.45rem;
  border-radius: 0.4rem;
  font-size: 0.72rem;
  font-weight: 600;
  letter-spacing: 0.01em;
  color: var(--mg-muted);
}
.mg-more-toggle:hover { color: var(--mg-accent); background: var(--mg-inset); }
.mg-more-toggle.is-on { color: var(--mg-accent); }
/* The caret turns rather than swapping glyphs: one element, no reflow. */
.mg-more-caret { transition: transform 0.15s ease; }
.mg-more-caret.is-open { transform: rotate(180deg); }
.mg-more-rows {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.3rem 0.75rem;
  margin-top: 0.3rem;
  padding: 0.35rem;
  border-radius: 0.6rem;
  border: 1px solid var(--mg-border);
  background: var(--mg-card);
}
.mg-tool-group {
  display: inline-flex;
  align-items: center;
  gap: 0.2rem;
}
/* The type's name, so a borrowed tool always says whose it is. */
.mg-tool-group-name {
  margin-right: 0.15rem;
  font-size: 0.66rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--mg-faint);
  white-space: nowrap;
}
@media (max-width: 640px) {
  /* On a phone the groups stack and the names carry the reading order. */
  .mg-more-rows { gap: 0.25rem 0.5rem; }
  .mg-tool-group { flex: 1 1 100%; }
}

/* ---- Floor tabs: dragging and renaming -----------------------------
   The slot is what carries the insertion mark, so the mark does not move the tabs while a
   drag is in flight: a bar that reflows under the pointer is a bar nobody can aim at. */
.mg-floor-slot { display: inline-flex; align-items: center; border-left: 2px solid transparent; }
.mg-floor-slot.is-drop { border-left-color: var(--mg-accent); }
.mg-floor.is-dragging { opacity: 0.45; }
.mg-floor { touch-action: none; }
.mg-floor-name {
  width: 7rem;
  padding: 0.15rem 0.35rem;
  border-radius: 0.35rem;
  border: 1px solid var(--mg-accent);
  background: var(--mg-inset);
  color: var(--mg-text);
  font-size: 0.75rem;
}

/* ---- Status line ------------------------------------------------- */
.mg-status {
  display: flex;
  flex-wrap: wrap;
  gap: 0.35rem;
  align-items: baseline;
  margin-top: 0.5rem;
  font-size: 0.7rem;
  color: var(--mg-faint);
}
.mg-status-seed { font-family: "JetBrains Mono", ui-monospace, monospace; color: var(--mg-muted); }
.mg-status-sep { opacity: 0.6; }
.mg-status-note { color: var(--mg-muted); font-variant-numeric: tabular-nums; }
.mg-status-dirty { color: var(--mg-accent); }
.mg-status-err { color: var(--mg-danger); }

/* ---- The key sheet ----------------------------------------------- */
/* The names switch, above the rows it governs. A little air under it so it reads as a
   heading for the list rather than as the list's first entry. */
.mg-key-check { margin-bottom: 0.5rem; }

.mg-empty { font-size: 0.78rem; font-style: italic; color: var(--mg-faint); }
/* The labels block sits under the key on the same card: they are both "what this map says",
   and a second card for two text boxes would read as a second panel. */
.mg-key-labels { margin-top: 1rem; padding-top: 0.75rem; border-top: 1px solid var(--mg-border); }
.mg-key-labels .mg-key-list { max-height: 14rem; }
.mg-key-drop {
  flex: 0 0 auto;
  align-self: flex-start;
  color: var(--mg-faint);
  padding: 0.15rem;
  border-radius: 0.375rem;
}
.mg-key-drop:hover { color: var(--mg-danger); background: var(--mg-inset); }
.mg-key-list { display: flex; flex-direction: column; gap: 0.5rem; max-height: 32rem; overflow-y: auto; }
/* THESE TWO LIVE HERE, BELOW THE BASE RULE, AND NOT UP WITH THE OTHER PANE RULES. A
   `@container` block adds no specificity, so the 32rem above — declared later in the file —
   silently beat them where they used to sit, and the three-pane key card carried a nested
   scrollbar the whole time despite the comment there saying it must not. Same trap as a media
   query; the cascade does not care which kind of at-rule you wrote. */
@container mg (min-width: 860px) {
  /* Under the map: bounded and calm. A 32rem list there reads as a runaway. */
  .mg-key-list { max-height: 20rem; }
}
@container mg (min-width: 1180px) {
  /* In its own column the card is already window-height and scrolls itself, so the list
     fills it — two nested scrollbars on one card is the worst of both. */
  .mg-key-list { max-height: none; overflow-y: visible; }
}
.mg-key-row {
  display: flex;
  gap: 0.5rem;
  padding: 0.4rem;
  border-radius: 0.5rem;
  border: 1px solid transparent;
}
.mg-key-row:hover { background: var(--mg-inset); }
.mg-key-row.is-on { border-color: var(--mg-accent-border); background: var(--mg-accent-soft); }
.mg-key-n {
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.5rem;
  height: 1.5rem;
  border-radius: 9999px;
  border: 1px solid var(--mg-border-strong);
  font-size: 0.7rem;
  font-weight: 700;
  color: var(--mg-accent);
  font-variant-numeric: tabular-nums;
}
/* ---- Dragging a key row to renumber it ------------------------------
   The SLOT carries the insertion mark, not the row, so the mark cannot move the list under the
   pointer that is aiming at it — the same bargain `.mg-floor-slot` makes for the tabs, and the
   transparent border is what reserves the space whether or not a drag is in flight.
   The badge is the handle: `touch-action: none` so a drag on a phone reorders instead of
   scrolling the key, and no text selection while it travels. */
.mg-key-slot { display: block; border-top: 2px solid transparent; }
.mg-key-slot.is-drop { border-top-color: var(--mg-accent); }
.mg-key-row.is-dragging { opacity: 0.45; }
.mg-key-n { cursor: grab; touch-action: none; user-select: none; }
.mg-key-body { flex: 1 1 auto; min-width: 0; }
.mg-key-name,
.mg-key-desc {
  width: 100%;
  background: transparent;
  border: none;
  color: var(--mg-text);
  font-size: 0.78rem;
  padding: 0.1rem 0.15rem;
  border-radius: 0.3rem;
  /* No drag handle: the description box used to be `resize: vertical`, and a pull on its
     corner could stretch one row to a page. It is a fixed three lines now, with its own
     scrollbar when the (capped) text needs one. */
  resize: none;
}
.mg-key-name { font-family: "Cinzel", serif; font-weight: 700; }
.mg-key-desc {
  color: var(--mg-muted);
  font-size: 0.72rem;
  line-height: 1.35;
  height: calc(3 * 1.35 * 0.72rem + 0.2rem);
  overflow-y: auto;
}
.mg-key-name:focus,
.mg-key-desc:focus { outline: none; background: var(--mg-inset); box-shadow: 0 0 0 1px var(--mg-accent-border); }

/* ---- The Objects panel ------------------------------------------- */
/* Everything a map type can PLACE, as pictures rather than as a strip of chips: forty-six
   dungeon props in a scrolling ribbon is a lucky dip, and a field map's eleven scenes had no
   drawing at all and showed as plain words. Grouped, searchable, and every tile painted by
   the type's own painter (ui/palette.js::objectThumb). */
.mg-objects {
  display: flex;
  flex-direction: column;
  min-width: 0;
}
.mg-obj-search {
  width: 100%;
  margin: 0.1rem 0 0.6rem;
  padding: 0.35rem 0.55rem;
  font-size: 0.78rem;
  border-radius: 0.5rem;
  border: 1px solid var(--mg-border);
  background: var(--mg-inset);
  color: var(--mg-text);
}
.mg-obj-search::placeholder { color: var(--mg-faint); }
.mg-obj-search:focus { outline: none; border-color: var(--mg-accent-border); box-shadow: 0 0 0 1px var(--mg-accent-border); }
/* The grid scrolls, the card does not: in three panes the card is already window-height and
   two nested scrollbars on one card is the worst of both. The cap is what keeps the panel a
   panel on a phone, where the map is what the fold is for. */
.mg-obj-groups {
  flex: 1 1 auto;
  min-height: 0;
  max-height: 24rem;
  overflow-y: auto;
}
@container mg (min-width: 1180px) {
  .mg-obj-groups { max-height: none; }
}
.mg-obj-group + .mg-obj-group { margin-top: 0.75rem; }
.mg-obj-title {
  font-family: "Cinzel", serif;
  font-size: 0.66rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--mg-faint);
  margin-bottom: 0.35rem;
}
/* auto-fill rather than a fixed count: the panel is 13.5rem at its narrowest and 18rem at its
   widest, and the tiles should reflow with it rather than being squeezed. */
.mg-obj-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(4.5rem, 1fr));
  gap: 0.3rem;
}
.mg-obj {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.2rem;
  /* 44px is the touch target the whole tile has to clear, thumbnail and caption together. */
  min-height: 4.6rem;
  padding: 0.3rem 0.15rem 0.25rem;
  border-radius: 0.5rem;
  border: 1px solid transparent;
  color: var(--mg-muted);
  font-size: 0.62rem;
  line-height: 1.15;
  text-align: center;
}
.mg-obj:hover { border-color: var(--mg-accent-border); color: var(--mg-accent); background: var(--mg-inset); }
.mg-obj:focus-visible { outline: none; border-color: var(--mg-accent); box-shadow: 0 0 0 1px var(--mg-accent-border); }
.mg-obj.is-on { border-color: var(--mg-accent); background: var(--mg-accent-soft); color: var(--mg-accent); }
/* 56px is OBJECT_THUMB_PX in ui/palette.js; the canvas is drawn at device resolution and
   sized back down here, so a farmstead is crisp on a phone. */
.mg-obj-thumb {
  display: block;
  width: 56px;
  height: 56px;
  max-width: 100%;
  border-radius: 0.3rem;
  background: var(--mg-paper);
  box-shadow: inset 0 0 0 1px var(--mg-paper-edge);
}
/* TWO LINES, then an ellipsis. "Standing stones" and "Armour stand" both need the second
   line, and a tile that grew to three would make its row taller than every other tile in it. */
.mg-obj-name {
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  font-weight: 600;
}

/* ---- Modals ------------------------------------------------------ */
/* z-index 60, the modal rung of DESIGN.md's stacking ladder. */
.mg-modal { position: fixed; inset: 0; z-index: 60; display: flex; align-items: center; justify-content: center; padding: 1rem; }
.mg-modal-scrim { position: absolute; inset: 0; background: rgba(0, 0, 0, 0.6); }
.mg-modal-card {
  position: relative;
  width: 100%;
  max-width: 30rem;
  max-height: 85vh;
  overflow-y: auto;
  padding: 1.25rem;
  border-radius: 1rem;
  border: 1px solid var(--mg-border);
  background: var(--mg-card);
  box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5);
}
.mg-export-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 0.5rem; margin-top: 0.75rem; }
/* The export dialog's card of choices is wider than the other modals: six pictures need
   room, and on a laptop three across is the shape that lets the eye compare them. */
.mg-modal-card-wide { max-width: 46rem; }

/* Two columns down to a 375 px phone, three from `sm` up. Hand-written rather than Tailwind
   utilities on purpose: this file ships as-is, and a new utility class silently does nothing
   until `make css` runs (CLAUDE.md). */
.mg-x-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 0.5rem;
  margin-top: 0.75rem;
}
@media (min-width: 640px) {
  .mg-x-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}
.mg-x-card {
  display: flex;
  flex-direction: column;
  gap: 0.3rem;
  padding: 0.5rem;
  border-radius: 0.7rem;
  border: 1px solid var(--mg-border);
  background: var(--mg-inset);
  cursor: pointer;
}
.mg-x-card:hover { border-color: var(--mg-accent-border); }
.mg-x-card.is-on {
  border-color: var(--mg-accent);
  background: var(--mg-accent-soft);
}
/* The focus ring lands on the card, not on the radio dot inside it: the whole card is the
   hit area, so the whole card is what a keyboard user needs to see selected. */
.mg-x-card:focus-within { box-shadow: 0 0 0 2px var(--mg-accent); }
.mg-x-thumb {
  display: block;
  border-radius: 0.45rem;
  overflow: hidden;
  border: 1px solid var(--mg-border);
  line-height: 0;
}
/* The canvas keeps its own 148 x 88 size and centres, rather than stretching to the card.
   Stretching looked fine until the modal got wider and every thumbnail grew to 153 px tall,
   which is how a six-card grid ends up 250 px taller than the viewport. */
.mg-x-thumb canvas { display: block; width: 100%; max-width: 148px; height: auto; margin: 0 auto; }
.mg-x-pick { display: flex; align-items: center; gap: 0.4rem; }
.mg-x-pick input { accent-color: var(--mg-accent); flex: none; }
.mg-x-name { font-size: 0.78rem; font-weight: 700; color: var(--mg-text); line-height: 1.2; }
.mg-x-hint { font-size: 0.66rem; line-height: 1.3; color: var(--mg-muted); }
.mg-x-meta { font-size: 0.64rem; line-height: 1.3; color: var(--mg-faint); font-variant-numeric: tabular-nums; }
/* The card grid can outgrow a short laptop viewport, and a Download button below the fold of
   a scrolling modal reads as a dialog with no way to finish. The actions stay put; the
   negative offset pulls the strip over the card's own bottom padding so nothing shows under
   it as the grid scrolls past. */
.mg-modal-card-wide .mg-modal-actions {
  position: sticky;
  bottom: -1.25rem;
  margin-bottom: -1.25rem;
  padding: 0.6rem 0 1.25rem;
  background: var(--mg-card);
  border-top: 1px solid var(--mg-border);
}
.mg-modal-actions { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: 0.5rem; margin-top: 1rem; }

/* ---- Import dialog ----------------------------------------------------- */
/* The drop zone is a BUTTON as well as a drop target: a phone has nothing to drag from and a
   keyboard cannot drop, so the same box has to be clickable and focusable. Dashed, because a
   dashed rectangle is the one shape everybody already reads as "put a file here" (DESIGN.md's
   primary empty state uses the same border). */
.mg-drop {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 0.3rem;
  width: 100%;
  margin-top: 0.75rem;
  padding: 1.4rem 1rem;
  border-radius: 0.9rem;
  border: 2px dashed var(--mg-border-strong);
  background: var(--mg-inset);
  color: var(--mg-muted);
  text-align: center;
  cursor: pointer;
}
.mg-drop:hover,
.mg-drop:focus-visible { border-color: var(--mg-accent); color: var(--mg-text); outline: none; }
.mg-drop.is-over { border-color: var(--mg-accent); background: var(--mg-accent-soft); color: var(--mg-text); }
.mg-drop-icon { width: 1.6rem; height: 1.6rem; }
.mg-drop-title { font-size: 0.85rem; font-weight: 700; color: var(--mg-text); }
.mg-drop-sub { font-size: 0.72rem; color: var(--mg-faint); }
/* What was picked, shown before it is allowed to replace anything on screen. */
.mg-import-card {
  margin-top: 0.75rem;
  padding: 0.6rem 0.75rem;
  border-radius: 0.7rem;
  border: 1px solid var(--mg-accent-border);
  background: var(--mg-accent-soft);
}
.mg-import-name { font-family: "Cinzel", serif; font-weight: 700; font-size: 0.9rem; color: var(--mg-text); margin: 0; }
.mg-import-line { font-size: 0.72rem; line-height: 1.35; color: var(--mg-muted); margin: 0.15rem 0 0; }
.mg-import-file { font-family: "JetBrains Mono", ui-monospace, monospace; font-size: 0.66rem; color: var(--mg-faint); word-break: break-all; }
/* An error here is a sentence and a next step, never a stack trace — see doc/inspect.js. */
.mg-import-error {
  margin-top: 0.75rem;
  padding: 0.6rem 0.75rem;
  border-radius: 0.7rem;
  border: 1px solid rgba(220, 38, 38, 0.45);
  background: rgba(220, 38, 38, 0.12);
}
.mg-import-error-head { font-size: 0.78rem; font-weight: 700; line-height: 1.35; color: #fca5a5; margin: 0; }
/* Light mode is `html:not(.dark)`, the same gate the `--mg-*` block above uses; a red that
   reads on stone-900 is invisible on parchment. */
html:not(.dark) [data-mapgen] .mg-import-error-head { color: #991b1b; }
html:not(.dark) [data-mapgen] .mg-import-error { border-color: rgba(153, 27, 27, 0.4); background: rgba(153, 27, 27, 0.1); }
/* The shortcuts sheet is a two-column GRID: the keys take exactly the width the widest key
   needs, and every sentence starts on the same line, so a long key label no longer pushes
   its sentence away from the others. The rows are `display: contents` so the dt/dd pairs
   fall into the grid's columns. Twice the width on a desktop; a phone keeps the card. */
.mg-keys {
  display: grid;
  grid-template-columns: max-content minmax(0, 1fr);
  column-gap: 1rem;
  row-gap: 0.5rem;
  align-items: baseline;
  margin-top: 0.75rem;
}
.mg-keys-row { display: contents; }
.mg-keys .mg-kbd { white-space: normal; max-width: 18rem; }
.mg-keys-what { font-size: 0.78rem; color: var(--mg-muted); }
.mg-modal-card-keys { max-width: 60rem; }
.mg-modal-close {
  position: absolute;
  top: 0.75rem;
  right: 0.75rem;
  width: 2rem;
  height: 2rem;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: 0.5rem;
  border: 1px solid transparent;
  color: var(--mg-muted);
  background: transparent;
}
.mg-modal-close:hover { color: var(--mg-accent); border-color: var(--mg-accent-border); background: var(--mg-inset); }
.mg-modal-close svg { width: 1.1rem; height: 1.1rem; }
@media (max-width: 640px) {
  .mg-keys { grid-template-columns: minmax(6rem, max-content) minmax(0, 1fr); column-gap: 0.6rem; }
  .mg-keys .mg-kbd { max-width: 9rem; }
}
.mg-kbd {
  display: inline-block;
  min-width: 6.5rem;
  padding: 0.1rem 0.4rem;
  border-radius: 0.3rem;
  border: 1px solid var(--mg-border-strong);
  background: var(--mg-inset);
  color: var(--mg-text);
  font-family: "JetBrains Mono", ui-monospace, monospace;
  font-size: 0.68rem;
}
.mg-foot { margin-top: 1rem; font-size: 0.72rem; color: var(--mg-faint); }

/* Reduced motion: the only transition here is a border colour on hover, which is already
   exempt from DESIGN.md's "never transition a theme-swapped colour" rule — but the block
   is kept explicit so a later animation cannot slip in without someone reading it. */
@media (prefers-reduced-motion: reduce) {
  [data-mapgen] * { transition: none !important; }
}
