/*
  Shared stylesheet for index.html and comics.html (the "medieval
  grimoire" design system). Both pages use the same shell markup:
    .stage (one column) > .sidebar (sticky nav bar) + .main-card
  reader.html also loads this file — only for the design tokens below and
  the .logo rule — but defines its own full-height layout in reader.css.

  See ../README.md for the full site overview and the "Design system"
  design notes (why the nav bar/main-card are physically separate cards,
  which parts are real image assets vs. CSS, etc).
*/

/* Pirata One and EB Garamond, served from this repo rather than from
   Google. This line used to fetch them from fonts.googleapis.com, which
   handed every visitor's IP address to Google before the page rendered —
   the exact arrangement a German court ruled against in January 2022.
   css/fonts/fonts.css has the whole reasoning and says how to
   regenerate it; it is generated, so don't hand-edit it. */
@import url('fonts/fonts.css');

/* ---- Design tokens ---- */
:root {
  --parchment: #cbb284;       /* card background */
  --parchment-light: #ead3a2; /* nested cards (grid items, comic covers) */
  --ink: #241a12;              /* body text */
  --gold: #c99a3e;             /* borders, accents, links-on-dark */
  --red: #8a2e22;              /* reserved, not currently used */
  --link: #2a5a8a;             /* text links */
  --muted: #6b5a42;            /* secondary/meta text */
}

* { box-sizing: border-box; }

/* The HTML `hidden` attribute only works because the browser's default
   stylesheet says [hidden] { display: none } — and ANY author rule that
   sets `display` on the same element beats it, silently. That is exactly
   how login.html came to show an "Admin" badge on an account with no
   admin rights: .account-badge sets `display: inline-block`, so setting
   .hidden = true on it did nothing at all visually.
   Stated once here, with !important, so hiding an element can never
   again be undone by that element's own styling. */
[hidden] { display: none !important; }

html {
  scroll-behavior: smooth;
}

/* Page background: a blurred/darkened comic panel (generated once from
   comics/party-meeting/1.jpg), shared by every page for the "archive
   library" atmosphere. Swap assets/background_blur.jpg to change it. */
body {
  margin: 0;
  color: var(--ink);
  font-family: 'EB Garamond', Georgia, serif;
  min-height: 100vh;
  background-image:
    linear-gradient(180deg, rgba(10,7,4,.55), rgba(10,7,4,.72)),
    url('../assets/background_blur.jpg');
  background-size: cover;
  background-position: center;
  background-attachment: fixed;
}

a { color: var(--link); }

.logo { display: block; text-align: center; }
.logo img { width: 150px; height: auto; filter: drop-shadow(0 5px 10px rgba(0,0,0,.4)); }

/* Top-level layout: the nav bar above the main content, as two distinct
   cards with a real gap between them (the dark background shows through
   — this is the "physically separate cards" requirement, not just two
   different background colors; stacked now instead of side by side). */
.stage {
  width: 100%;
  margin: 0 0 34px;
  display: grid;
  grid-template-columns: 1fr;
  gap: 34px;
  align-items: start;
}
/* Everything under the nav bar keeps the old centred width; only the
   bar itself runs edge to edge. */
.stage > :not(.sidebar) {
  width: min(1580px, 96%);
  justify-self: center;
}

/* --- nav bar: docked to the top edge, full width, always visible ---
   The element is still .sidebar (the class is in every page's markup,
   and in js/auth.js and js/search.js), but it is a horizontal bar now.
   Separated from the page by the gold border and the 8px dark band
   under it (the same "ring" as the cards, only on the bottom side).

   Always visible: the logo, the search box (js/search.js), the account
   link (js/auth.js) and the "Menu" button (end of js/search.js).
   Everything else — the nav links and the rest of the account block —
   is the menu, hidden until the button sets .nav-open. */
.sidebar {
  position: sticky;
  top: 0;
  z-index: 50;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px 18px;
  background: var(--parchment);
  border-bottom: 2px solid var(--gold);
  box-shadow: 0 8px 0 rgba(20,14,8,.35), 0 20px 50px rgba(0,0,0,.4), inset 0 0 30px rgba(90,60,20,.08);
  /* Contents line up with the card below: same centred width, plus
     the bar's own 24px. */
  padding: 10px calc((100% - min(1580px, 96%)) / 2 + 24px);
}

/* Anchor jumps (#section links) land below the bar, not under it. */
html { scroll-padding-top: 140px; }

/* Always visible, in this order. Equal orders fall back to markup
   order, which the scripts set: account link, then the button. */
.sidebar .logo { order: 0; }
.sidebar .logo img { width: 90px; }
.sidebar-search { order: 1; flex: 1 1 200px; max-width: 360px; margin: 0 0 0 auto; }
.sidebar > .sidebar-account-link {
  order: 2;
  max-width: 180px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.nav-toggle { order: 2; }

/* Everything else is the menu. Selector weight is deliberately high so
   it beats ".sidebar a { display: block }"; [hidden] still wins
   through its !important. */
.sidebar > :not(.corner):not(.logo):not(.sidebar-search):not(.sidebar-account-link):not(.nav-toggle) { order: 4; }
.sidebar:not(.nav-open) > :not(.corner):not(.logo):not(.sidebar-search):not(.sidebar-account-link):not(.nav-toggle) { display: none; }

/* A gold rule between the bar and the open menu. It is a flex item
   taking a full line (order 3, between the button and the menu), which
   is also what forces the menu onto its own row. */
.sidebar.nav-open::after {
  content: "";
  order: 3;
  flex-basis: 100%;
  height: 1px;
  background: var(--gold);
  opacity: .6;
}

/* The menu button: same parchment/gold treatment as the search field. */
.nav-toggle {
  font-family: 'Pirata One', serif;
  font-size: 17px;
  letter-spacing: .5px;
  color: #4a3413;
  background: rgba(255,255,255,.3);
  border: 1px solid var(--gold);
  border-radius: 2px;
  padding: 5px 12px;
  cursor: pointer;
}
.nav-toggle:hover,
.nav-toggle[aria-expanded="true"] { background: rgba(201,154,62,.28); }
.nav-toggle:focus-visible {
  outline: none;
  border-color: #8a6a28;
  box-shadow: 0 0 0 2px rgba(201,154,62,.35);
}

/* In the open menu (wide screens), each heading labels its group on one
   row; the second group is set apart by a gold rule. */
.sidebar h3 {
  font-family: 'Pirata One', serif;
  font-size: 16px;
  font-weight: 400;
  color: #4a3413;
  margin: 0;
  letter-spacing: .5px;
}
.sidebar h3 ~ h3 {
  margin-left: 6px;
  padding-left: 24px;
  border-left: 1px solid var(--gold);
}

.nav-divider {
  display: none;              /* shown again in the phone menu */
  width: 100%;
  height: 14px;
  background-image: url('../assets/Border_nav.png');
  background-size: contain;
  background-repeat: no-repeat;
  margin-bottom: 4px;
  opacity: .9;
}

.sidebar a { display: block; color: var(--link); text-decoration: none; font-size: 15px; padding: 4px 0; }
.sidebar a:hover { text-decoration: underline; }

/* Category link with no destination page yet (e.g. Bestiary) — shown as
   plain muted text instead of an <a> so it doesn't 404. Swap the <span>
   for an <a href="..."> in index.html/comics.html/characters.html once
   the page exists (see how "Characters" was promoted from this same
   pattern). */
.sidebar-link-disabled {
  display: block;
  color: var(--muted);
  font-size: 15px;
  padding: 4px 0;
  opacity: .7;
  cursor: default;
}

/* --- main: its own physically separate card, with ornate corners --- */
.main-card {
  position: relative;
  background: var(--parchment);
  background-image:
    radial-gradient(circle at 15% 10%, rgba(255,255,255,.25), transparent 40%),
    radial-gradient(circle at 90% 85%, rgba(0,0,0,.08), transparent 45%);
  border: 2px solid var(--gold);
  box-shadow: 0 0 0 8px rgba(20,14,8,.35), 0 30px 80px #000a, inset 0 0 60px rgba(90,60,20,.12);
  padding: 44px 50px 50px;
  min-width: 0;
}

.corner {
  position: absolute;
  width: 120px;
  height: 170px;
  pointer-events: none;
  background-image: url('../assets/Corner.png');
  background-size: contain;
  background-repeat: no-repeat;
  filter: drop-shadow(0 2px 4px rgba(0,0,0,.5));
}
.corner.tl { top: -16px; left: -16px; }
.corner.tr { top: -16px; right: -16px; transform: scaleX(-1); }
.corner.bl { bottom: -16px; left: -16px; transform: scaleY(-1); }
.corner.br { bottom: -16px; right: -16px; transform: scale(-1,-1); }

/* No corner ornaments on the bar: at full width they would sit on the
   window edges. The card below keeps its own. (The four corner divs are
   still in every page's markup.) */
.sidebar .corner { display: none; }

.hero {
  padding-bottom: 26px;
  border-bottom: 1px solid var(--gold);
  margin-bottom: 30px;
}

.eyebrow {
  color: #8a5a1e;
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 3px;
  text-transform: uppercase;
  margin: 0 0 10px;
}

.hero h1 {
  font-family: 'Pirata One', serif;
  font-weight: 400;
  font-size: 44px;
  margin: 10px 0 14px;
  color: #2e2010;
  letter-spacing: .5px;
}

.hero p:not(.eyebrow) {
  max-width: 680px;
  margin: 0;
  line-height: 1.8;
  font-size: 17px;
}

.section { margin-top: 38px; }

.section-title {
  font-family: 'Pirata One', serif;
  font-weight: 400;
  font-size: 26px;
  color: #2e2010;
  padding-bottom: 10px;
  border-bottom: 1px solid var(--gold);
  margin-bottom: 26px;
  letter-spacing: .5px;
}

/* Homepage category grid (Characters, Bestiary, Races, Items, Geography,
   World Lore). Most are still plain, non-clickable .card divs — see the
   PLACEHOLDER comment in index.html. Characters is the first one wrapped
   in an <a> (its page now exists); .card's text-decoration/color reset
   below is what makes that safe to do for the others too, one at a time. */
.grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(170px, 1fr));
  gap: 18px;
}

.card {
  display: block;
  background: var(--parchment-light);
  border: 1px solid var(--gold);
  padding: 22px 16px 18px;
  text-align: center;
  text-decoration: none;
  color: inherit;
  position: relative;
  transition: transform .15s;
}
.card:hover { transform: translateY(-3px); }

.card .cube {
  width: 30px;
  height: 30px;
  margin: 0 auto 8px;
  background-image: url('../assets/Cube.png');
  background-size: contain;
  background-repeat: no-repeat;
}

.card h3, .card h4 {
  font-family: 'Pirata One', serif;
  font-weight: 400;
  font-size: 17px;
  margin: 0 0 6px;
  color: #2e2010;
  letter-spacing: .3px;
}
.card p { font-size: 12.5px; color: var(--muted); line-height: 1.5; margin: 0; }

.featured-section { margin-top: 8px; }

.featured-card {
  display: flex;
  align-items: center;
  gap: 22px;
  background: var(--parchment-light);
  border: 1px solid var(--gold);
  padding: 26px;
  text-decoration: none;
  color: inherit;
  transition: transform .15s;
}
.featured-card:hover { transform: translateY(-3px); }

.featured-card .sigil {
  width: 64px;
  height: 64px;
  border-radius: 50%;
  flex-shrink: 0;
  background: var(--parchment-light);
  border: 2px solid var(--gold);
  display: flex;
  align-items: center;
  justify-content: center;
}
.featured-card .sigil img { width: 36px; height: auto; }

.featured-card h3 {
  font-family: 'Pirata One', serif;
  font-weight: 400;
  font-size: 22px;
  margin: 0 0 6px;
  color: #2e2010;
  letter-spacing: .5px;
}
.featured-card p { font-size: 15px; line-height: 1.6; margin: 0; max-width: 640px; }

@media (max-width: 600px) {
  .featured-card { flex-direction: column; text-align: center; }
}

/* Below 800px: tighter card padding, and the nav bar rearranged for a
   phone — logo, account link and button on the first row, search on the
   second, and the open menu as a list under them. */
@media (max-width: 800px) {
  .main-card { padding: 34px 24px; }
  .sidebar { padding: 10px calc(2% + 12px); gap: 6px 12px; }
  .sidebar .logo img { width: 70px; }
  .sidebar > .sidebar-account-link { order: 1; margin-left: auto; max-width: 40vw; }
  .nav-toggle { order: 1; }
  .sidebar-search { order: 2; flex-basis: 100%; max-width: none; margin: 0; }
  .sidebar.nav-open > :not(.corner):not(.logo):not(.sidebar-search):not(.sidebar-account-link):not(.nav-toggle) { flex-basis: 100%; }
  .sidebar h3 { text-align: center; margin-top: 8px; }
  .sidebar h3 ~ h3 { margin-left: 0; padding-left: 0; border-left: 0; }
  .sidebar .nav-divider { display: block; }
  /* Two classes: the plain .sidebar-account rule sits further down the
     file and would win a one-class tie. */
  .sidebar > .sidebar-account { display: block; margin: 0; }
  .corner { width: 80px; height: 110px; }
  .hero h1 { font-size: 30px; }
  .grid { grid-template-columns: repeat(auto-fill, minmax(145px, 1fr)); }
}

/* ---- comics.html: library header + sort toolbar + grid ---- */

.section-heading { border-bottom: 1px solid var(--gold); margin-bottom: 25px; padding-bottom: 10px; }
.section-heading h1 {
  font-family: 'Pirata One', serif;
  font-weight: 400;
  font-size: 36px;
  margin: 8px 0 6px;
  color: #2e2010;
  letter-spacing: .5px;
}
.muted { color: var(--muted); margin-top: -5px; }

.library-toolbar {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 8px 16px;
  margin-bottom: 25px;
  font-size: 14px;
}
.library-toolbar select {
  border: 1px solid var(--gold);
  background: var(--parchment-light);
  color: var(--ink);
  padding: 8px 10px;
  font-family: 'EB Garamond', serif;
}
/* #side-group-toggle's wrapper (comics.html) — a checkbox + its own
   label, styled as one clickable unit sitting alongside the "Sort by"
   pair in the same .library-toolbar. Independent of that select: toggling
   this never resets the sort choice, and vice versa — see
   renderSideStories() in js/comics.js. */
.toggle-field {
  display: flex;
  align-items: center;
  gap: 6px;
  cursor: pointer;
}
.toggle-field input[type="checkbox"] {
  width: 15px;
  height: 15px;
  accent-color: var(--gold);
  cursor: pointer;
}

/* .character-combo: the character-search wrapper (comics.html — used by
   both Side Stories' #character-search and Non-Canon's
   #noncanon-character-search, same markup/classes, independent ids) — a
   plain text input plus a JS-driven dropdown list (.character-combo-list),
   standing in for a native <input list>/<datalist> since that can't show
   live substring-filtered multiple matches (e.g. typing "alex" surfacing
   both "Alexandra" and "Alexander") — see js/comics.js's
   createThreadSection() (renderDropdown()/setupSearch()). */
.character-combo {
  position: relative;
  display: inline-block;
}
.character-combo input[type="search"] {
  border: 1px solid var(--gold);
  background: var(--parchment-light);
  color: var(--ink);
  padding: 8px 10px;
  font-family: 'EB Garamond', serif;
  font-size: 14px;
  width: 200px;
}
/* Dropdown panel: absolutely positioned under the input so it floats over
   the comic grid below rather than pushing it down. */
.character-combo-list {
  position: absolute;
  top: calc(100% + 4px);
  left: 0;
  min-width: 100%;
  max-height: 220px;
  overflow-y: auto;
  margin: 0;
  padding: 4px 0;
  list-style: none;
  background: var(--parchment-light);
  border: 1px solid var(--gold);
  box-shadow: 0 8px 20px rgba(0,0,0,.35);
  z-index: 20;
}
.character-combo-list li {
  padding: 7px 12px;
  font-size: 14px;
  cursor: pointer;
  white-space: nowrap;
}
.character-combo-list li:hover { background: rgba(201,154,62,.3); }
.character-combo-list .character-combo-empty {
  color: var(--muted);
  font-style: italic;
  cursor: default;
}
.character-combo-list .character-combo-empty:hover { background: none; }

/* .comic-grid is populated at runtime by renderLibrary() in js/comics.js;
   .comic-card markup comes from that same file's createCard(). */
.comic-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
  gap: 18px;
}
.comic-card {
  background: var(--parchment-light);
  border: 1px solid var(--gold);
  overflow: hidden;
  display: block;
  color: inherit;
  text-decoration: none;
  transition: transform .15s;
}
.comic-card:hover { transform: translateY(-3px); }
/* No background fill: assets/Cube.png (the missing-image placeholder,
   see js/comics.js) is itself a transparent PNG, and a solid fill behind
   it looked like a dull box around the cube instead of it just floating
   on the card. */
.comic-cover { width: 100%; aspect-ratio: 2/3; display: block; object-fit: cover; }
.comic-card-body { padding: 12px; }
.comic-card h3 {
  font-family: 'Pirata One', serif;
  font-weight: 400;
  margin: 0 0 6px;
  font-size: 16px;
  color: #2e2010;
}
.comic-meta { color: var(--muted); font-size: 12px; }

/* ---- comics.html: Main Story / Side Stories / Non-Canon split ----
   See js/comics.js's renderLibrary()/renderMainStory()/createThreadSection().
   Each .library-section is a fixed block (Main Story, then Side Stories,
   then Non-Canon) — not reader-togglable, that split itself is an
   editorial fact driven by the comics table's "thread" column. Side
   Stories and Non-Canon each have their own "Group by" toolbar
   (same markup/classes, independent ids — see comics.html). */
.library-section { margin-bottom: 40px; }
.library-section:last-child { margin-bottom: 0; }

.library-section-header {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 8px 20px;
  border-bottom: 1px solid var(--gold);
  margin-bottom: 18px;
  padding-bottom: 8px;
}
.library-section-heading {
  font-family: 'Pirata One', serif;
  font-weight: 400;
  font-size: 22px;
  margin: 0;
  color: #2e2010;
  letter-spacing: .3px;
  display: flex;
  align-items: center;
  gap: 8px;
  cursor: pointer;
  user-select: none;
}
/* Sits inside .library-section-header (Side Stories and Non-Canon) —
   same toolbar look as the old top-of-page sort control, just without
   its own margin/border since .library-section-header already provides
   both. */
.library-section-header .library-toolbar { margin-bottom: 0; }

/* ---- Collapsible sections + series-groups ----
   Click a .library-section-heading (Main Story/Side Stories/Non-Canon)
   or a .series-group-heading (one per series, grouped view only) to hide
   its comics — wireSectionCollapse()/render() in js/comics.js toggle
   .section-collapsed / .group-collapsed, pure CSS state, same "no
   persistence" pattern as reader.html's collapsible sidebars. Section
   collapse state survives re-renders (the .library-section wrapper is
   static, JS only ever replaces .library-section-body's contents);
   series-group collapse state doesn't (each .series-group is torn down
   and rebuilt from scratch on every render() call) — an intentional
   trade-off, not a bug: groups only exist in the "Group by series" view
   to begin with, so losing their collapsed state whenever that view is
   rebuilt (a new sort, a new search) reads as "the view changed", not as
   forgetfulness. */
.collapse-caret {
  display: inline-block;
  width: 0;
  height: 0;
  border-left: 5px solid transparent;
  border-right: 5px solid transparent;
  border-top: 7px solid var(--muted);
  flex-shrink: 0;
  transition: transform .15s;
}
.library-section.section-collapsed .collapse-caret,
.series-group.group-collapsed .collapse-caret {
  transform: rotate(-90deg);
}
.library-section.section-collapsed .library-section-body,
.series-group.group-collapsed .comic-grid {
  display: none;
}
/* e.g. " (6)" next to a heading — how many comics are inside, visible
   whether the section/group is open or collapsed. Populated in
   js/comics.js (renderMainStory(), and createThreadSection()'s render()
   for both the section-level and each series-group's own count). */
.section-count, .series-group-count {
  font-family: 'EB Garamond', serif;
  font-weight: 400;
  font-size: 13px;
  color: var(--muted);
}

/* One labeled sub-grid per series, used only in Side Stories' default
   "Group by: Series" view — see renderSideStories() in js/comics.js. */
.series-group { margin-bottom: 26px; }
.series-group:last-child { margin-bottom: 0; }
.series-group-heading {
  font-family: 'EB Garamond', serif;
  font-weight: 700;
  font-size: 13px;
  letter-spacing: .6px;
  text-transform: uppercase;
  color: var(--muted);
  margin: 0 0 12px;
  display: flex;
  align-items: center;
  gap: 6px;
  cursor: pointer;
  user-select: none;
}
.series-group-heading .collapse-caret { border-top-color: var(--muted); }

/* ---- characters.html: portrait gallery ---- */

/* .character-grid is populated at runtime by renderCharacterGallery() in
   js/characters.js; .character-card markup comes from that same file's
   createCharacterCard(). Centered text (unlike .comic-card, which is
   left-aligned) — reads better under a portrait bust. */
.character-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
  gap: 18px;
}
.character-card {
  display: block;
  background: var(--parchment-light);
  border: 1px solid var(--gold);
  overflow: hidden;
  text-align: center;
  text-decoration: none;
  color: inherit;
  transition: transform .15s;
}
.character-card:hover { transform: translateY(-3px); }
/* No background fill — see the same note on .comic-cover above; applies
   here for the same reason (assets/Cube.png placeholder is transparent). */
.character-portrait { width: 100%; aspect-ratio: 4/6; display: block; object-fit: cover; }
.character-card-body { padding: 12px; }
.character-card h3 {
  font-family: 'Pirata One', serif;
  font-weight: 400;
  margin: 0 0 4px;
  font-size: 16px;
  color: #2e2010;
}
.character-meta { color: var(--muted); font-size: 12px; }

/* A linked character with no published page yet: the same card, greyed,
   and NOT an <a> — js/place-extras.js builds it as a <span>, so there is
   no href to disable and nothing to tab into. Mirrors
   .related-character-disabled in css/reader.css, which does this job for
   the comic reader's sidebar. The hover lift is cancelled deliberately:
   a card that rises under the pointer has promised a click. */
.character-card-disabled {
  cursor: default;
  color: var(--muted);
}
.character-card-disabled:hover { transform: none; }
.character-card-disabled .character-portrait { opacity: .55; }
.character-card-disabled h3 { color: var(--muted); }

/* One collapsible group per subcategory on characters.html — built by
   renderCharacterGallery() in js/characters.js, which is why
   #character-grid is a plain wrapper there and each group holds its own
   .character-grid.

   A PARALLEL set to .entity-category-group near the bottom of this file,
   not a reuse of it, for exactly the reason given in that section's
   header: .character-* and .entity-* stay free to evolve apart.
   .collapse-caret/.group-collapsed/.series-group-count ARE reused as-is —
   those three are already fully generic and shared with comics' series
   groups. Note the last rule has to name .character-grid: the entity
   version hides .entity-grid specifically, so it would collapse nothing
   here. */
.character-category-group { margin-bottom: 26px; }
.character-category-group:last-child { margin-bottom: 0; }
.character-category-heading {
  font-family: 'EB Garamond', serif;
  font-weight: 700;
  font-size: 13px;
  letter-spacing: .6px;
  text-transform: uppercase;
  color: var(--muted);
  margin: 0 0 12px;
  display: flex;
  align-items: center;
  gap: 6px;
  cursor: pointer;
  user-select: none;
}
.character-category-heading .collapse-caret { border-top-color: var(--muted); }
.character-category-group.group-collapsed .character-grid { display: none; }

/* ---- character.html: individual profile page ---- */

.back-link { margin: 0 0 24px; font-size: 14px; }
.back-link a { color: var(--muted); text-decoration: none; font-weight: 600; }
.back-link a:hover { color: var(--link); text-decoration: underline; }

/* Populated entirely by js/character.js from CHARACTERS (fetched from
   Supabase by js/data.js), looked up via the page's ?id= URL param — see that file for the "not
   found" fallback when the id doesn't match anything. Only wraps the
   infobox + name/title/bio now — "Appears in" (.character-appearances)
   is a sibling after this, not nested inside it, so it can span the full
   page width instead of just this row's body column.

   Layout: the infobox is a CSS float (not a flex column, like an earlier
   version of this page) so the name/title/bio genuinely wrap around it
   wiki-article-style, instead of being confined to a separate column
   that stops wherever the infobox does. .character-detail needs a
   clearfix (the ::after below) so its own box — and anything after it,
   like .character-appearances — accounts for the floated infobox's full
   height even when the bio text next to it is short. */
.character-detail::after {
  content: "";
  display: table;
  clear: both;
}
.character-infobox {
  float: left;
  width: 260px;
  margin: 0 36px 20px 0;
  background: var(--parchment-light);
  border: 1px solid var(--gold);
  box-shadow: 0 10px 30px rgba(0,0,0,.3);
}
.character-infobox-tabs {
  display: flex;
}
.character-infobox-tab {
  flex: 1;
  font-family: 'EB Garamond', serif;
  font-size: 12px;
  font-weight: 600;
  text-align: center;
  padding: 8px 4px;
  background: var(--parchment);
  color: var(--muted);
  border: none;
  border-bottom: 1px solid var(--gold);
  cursor: pointer;
}
.character-infobox-tab:hover { color: var(--ink); }
.character-infobox-tab.active {
  background: var(--parchment-light);
  color: #2e2010;
  border-bottom-color: transparent;
}
/* Same fixed box + object-fit: cover treatment the old single-image
   .character-detail-portrait used — every tab keeps this exact footprint
   regardless of the source image's own dimensions, so switching tabs
   never shifts the surrounding layout, and (per the same flex/aspect-
   ratio interaction noted lower down for .character-infobox) this only
   holds because .character-infobox itself is a float, never a flex item
   with stretch alignment. */
.character-infobox-image {
  width: 100%;
  aspect-ratio: 4/5;
  object-fit: cover;
  display: block;
  /* No background fill — see the same note on .comic-cover above. */
}
.character-infobox-stats {
  font-size: 13px;
}
.character-infobox-stat {
  display: flex;
  gap: 10px;
  padding: 8px 10px;
  border-top: 1px solid rgba(90,60,20,.2);
}
.character-infobox-stat:nth-child(odd) { background: rgba(255,255,255,.2); }
.character-infobox-stat-label {
  font-weight: 700;
  color: #2e2010;
  flex: 0 0 42%;
}
.character-infobox-stat-value { color: var(--ink); flex: 1; }
.character-detail-body h1 {
  font-family: 'Pirata One', serif;
  font-weight: 400;
  font-size: 38px;
  margin: 6px 0 4px;
  color: #2e2010;
  letter-spacing: .5px;
}
.character-detail-subtitle { color: var(--muted); font-size: 15px; margin: 0 0 20px; }

/* "From — Sunken Empire of Atlantia", one line per role, between the
   subtitle and the write-up.

   The lines are wrapped in a container rather than spaced individually,
   because the gap belongs AFTER THE GROUP: a character with three roles
   should read as one block, not three paragraphs. Doing it with
   :last-of-type on the line instead would have worked today and broken
   the day anything else in this column became a <p>. */
.character-detail-places { margin: 0 0 20px; }
.character-detail-place {
  font-size: 14px;
  margin: 0 0 4px;
  line-height: 1.5;
}
.character-detail-place:last-child { margin-bottom: 0; }
.character-detail-place-role {
  color: var(--muted);
  font-weight: 600;
}

/* character.bioHtml (see js/character.js) is real rendered Markdown —
   the database's bio_markdown can use headings, lists, bold/italic, etc.
   — so this styles whatever tags marked() produces, not just <p>. */
/* No max-width here on purpose (see below for the two things we tried
   and dropped):

   1. `max-width: fit-content`/`max-content` — broke wrapping outright.
      Those trigger shrink-to-fit sizing, which browsers compute ONCE
      from the (narrow) space beside the floated .character-infobox at
      the point a paragraph starts, then lock that in as the paragraph's
      box width for every line — including the ones below the infobox,
      where there's actually a full row of space. Result: text wrapped
      early and the line-below-the-float visibly misaligned/overflowed
      left, since its line box no longer lined up with the (too-narrow)
      box the browser had committed to. This bit us for real on
      Patience's bio (a multi-paragraph blockquote started beside the
      infobox and continued below it).
   2. `max-width: 680px` (matching .hero p's fixed 680px elsewhere) — no
      wrapping bug, but capped the text column well short of
      .character-detail-body's actual available width (.main-card can
      run past 1200px wide on a big screen), leaving a dead strip of
      parchment on the right that only grew with resolution.

   So: no max-width at all. A block with width:auto (the default, no
   shrink-to-fit involved) always fills its containing block, full stop
   — floats never shrink the BOX itself, they only eat into the
   available space of each LINE at that height, which is the normal/
   correct way text wraps around a float regardless of the box's width.
   That's why this doesn't reintroduce bug #1: fit-content/max-content
   are what trigger the once-computed shrink-to-fit box; plain auto
   width never does. Net effect: bio text hugs the infobox tightly
   where it floats beside it, then opens up to the full body width once
   it clears the infobox below — see site-conventions.md's "Character
   profile infobox" section. */
.character-detail-bio p { line-height: 1.75; margin: 0 0 14px; }
.character-detail-bio .muted { font-style: italic; margin: 0; }
.character-detail-bio h2, .character-detail-bio h3 {
  font-family: 'Pirata One', serif;
  font-weight: 400;
  color: #2e2010;
  letter-spacing: .3px;
}
.character-detail-bio h2 { font-size: 22px; margin: 26px 0 10px; }
.character-detail-bio h3 { font-size: 18px; margin: 20px 0 8px; }
.character-detail-bio h2:first-child, .character-detail-bio h3:first-child { margin-top: 0; }
.character-detail-bio ul, .character-detail-bio ol {
  margin: 0 0 14px;
  padding-left: 22px;
  line-height: 1.7;
}
.character-detail-bio li { margin-bottom: 4px; }
.character-detail-bio strong { color: #2e2010; }
.character-detail-bio blockquote {
  margin: 0 0 14px;
  padding-left: 16px;
  border-left: 3px solid var(--gold);
  color: var(--muted);
  font-style: italic;
}

/* "Appears in" reuses .library-toolbar/.comic-grid/.comic-card as-is (see
   comics.html's "library header + sort toolbar + grid" block above) — the
   only thing scoped here is the section wrapper itself. */
.character-appearances {
  margin-top: 28px;
  padding-top: 20px;
  border-top: 1px solid var(--gold);
}
.character-appearances h3 {
  font-family: 'Pirata One', serif;
  font-weight: 400;
  font-size: 18px;
  margin: 0 0 10px;
  color: #2e2010;
}
.character-appearances .library-toolbar { margin-bottom: 16px; }

@media (max-width: 600px) {
  /* Narrow viewport: drop the float and stack instead — a floated
     infobox with text wrapping beside it in a ~260px-or-less column
     reads badly, so this makes the infobox a normal centered block
     followed by the full-width bio, same stacking the old flex-column
     fallback did. */
  .character-infobox { float: none; width: 100%; max-width: 260px; margin: 0 auto 20px; }
}

/* ---- Generic entity system: Bestiary / Races / Items / World Lore ----
   These four content types are all structurally the same shape as each
   other (id/name/title/optional category/optional image/optional
   infobox+markdown write-up), so they share ONE list+detail system
   (js/entity-list.js, js/entity-detail.js) instead of four near-copies
   of the Characters system above.
   Comics and Characters keep their own dedicated .comic-* and .character-*
   styles/JS because they have real behavior beyond this shared shape —
   comics has dates/series/thread/sort/search, characters has the
   "Appears in" cross-link to comics — see site-conventions.md's "Generic
   entity system" section for the full rationale.

   Every rule below is deliberately a PARALLEL set to .character-grid/
   .character-card/.character-detail/.character-infobox* above, not a
   reuse of those classes, even though most of it is visually identical —
   this keeps the two systems free to evolve independently (e.g. if
   Characters ever grows a feature specific to people, or this system
   grows one specific to creatures/items/lore) without either one risking
   breaking the other. Categories (Monsters/Wildlife, Relics/Materials,
   Mechanics/Roles & Positions) reuse .collapse-caret/.group-collapsed/
   .series-group-count as-is, though — those are already fully generic
   (see their own comments further up), no reason to fork them too. */

/* ---- entity list pages (bestiary.html/races.html/items.html/world-lore.html) ---- */
.entity-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
  gap: 18px;
}
.entity-card {
  display: block;
  background: var(--parchment-light);
  border: 1px solid var(--gold);
  overflow: hidden;
  text-align: center;
  text-decoration: none;
  color: inherit;
  transition: transform .15s;
}
.entity-card:hover { transform: translateY(-3px); }
/* No background fill — assets/Cube.png (the missing-image placeholder)
   is itself transparent, see the same note on .comic-cover/.character-
   portrait above. */
.entity-image { width: 100%; aspect-ratio: 4/6; display: block; object-fit: cover; }
.entity-card-body { padding: 12px; }
.entity-card h3 {
  font-family: 'Pirata One', serif;
  font-weight: 400;
  margin: 0 0 4px;
  font-size: 16px;
  color: #2e2010;
}
.entity-meta { color: var(--muted); font-size: 12px; }

/* One collapsible group per subcategory — built by renderGroupedEntities()
   in js/entity-list.js, only for the types that have subcategories
   (Bestiary/Items/World Lore; Races has none, so races.html's grid is
   just .entity-grid directly with no grouping wrapper at all). Reuses
   .collapse-caret/.group-collapsed/.series-group-count as-is (see the
   comment above this whole section). */
.entity-category-group { margin-bottom: 26px; }
.entity-category-group:last-child { margin-bottom: 0; }
.entity-category-heading {
  font-family: 'EB Garamond', serif;
  font-weight: 700;
  font-size: 13px;
  letter-spacing: .6px;
  text-transform: uppercase;
  color: var(--muted);
  margin: 0 0 12px;
  display: flex;
  align-items: center;
  gap: 6px;
  cursor: pointer;
  user-select: none;
}
.entity-category-heading .collapse-caret { border-top-color: var(--muted); }
.entity-category-group.group-collapsed .entity-grid { display: none; }

/* ---- entity detail pages (creature.html/race.html/item.html/lore.html) ----
   Same wiki-infobox-float layout as .character-detail/.character-infobox*
   above (float left, clearfix, tabbed image gallery, optional stats
   table) — see that section's comments for the float/clearfix reasoning,
   it applies identically here. */
.entity-detail::after { content: ""; display: table; clear: both; }
.entity-infobox {
  float: left;
  width: 260px;
  margin: 0 36px 20px 0;
  background: var(--parchment-light);
  border: 1px solid var(--gold);
  box-shadow: 0 10px 30px rgba(0,0,0,.3);
}
.entity-infobox-tabs { display: flex; }
.entity-infobox-tab {
  flex: 1;
  font-family: 'EB Garamond', serif;
  font-size: 12px;
  font-weight: 600;
  text-align: center;
  padding: 8px 4px;
  background: var(--parchment);
  color: var(--muted);
  border: none;
  border-bottom: 1px solid var(--gold);
  cursor: pointer;
}
.entity-infobox-tab:hover { color: var(--ink); }
.entity-infobox-tab.active {
  background: var(--parchment-light);
  color: #2e2010;
  border-bottom-color: transparent;
}
.entity-infobox-image {
  width: 100%;
  aspect-ratio: 4/5;
  object-fit: cover;
  display: block;
}
.entity-infobox-stats { font-size: 13px; }
.entity-infobox-stat {
  display: flex;
  gap: 10px;
  padding: 8px 10px;
  border-top: 1px solid rgba(90,60,20,.2);
}
.entity-infobox-stat:nth-child(odd) { background: rgba(255,255,255,.2); }
.entity-infobox-stat-label { font-weight: 700; color: #2e2010; flex: 0 0 42%; }
.entity-infobox-stat-value { color: var(--ink); flex: 1; }

.entity-detail-body h1 {
  font-family: 'Pirata One', serif;
  font-weight: 400;
  font-size: 38px;
  margin: 6px 0 4px;
  color: #2e2010;
  letter-spacing: .5px;
}
.entity-detail-subtitle { color: var(--muted); font-size: 15px; margin: 0 0 4px; }
/* Shows the entry's resolved category (e.g. "Monsters") on its own detail
   page — categories aren't shown as a link anywhere, just this label, so
   unlike .entity-detail-subtitle there's no case where both need to look
   identical/interchangeable. Only rendered when non-blank (see
   js/entity-detail.js) — an uncategorized entry just skips this line. */
.entity-detail-category {
  display: inline-block;
  color: var(--muted);
  font-size: 12px;
  font-weight: 700;
  letter-spacing: .5px;
  text-transform: uppercase;
  margin: 0 0 16px;
}

/* Bio text: no max-width on purpose, from the start — see the long
   comment on .character-detail-bio p above for why (a block with default
   width:auto always fills its containing block regardless of a float
   beside it; only intrinsic-sizing keywords like fit-content/max-content
   break that). */
.entity-detail-bio p { line-height: 1.75; margin: 0 0 14px; }
.entity-detail-bio .muted { font-style: italic; margin: 0; }
.entity-detail-bio h2, .entity-detail-bio h3 {
  font-family: 'Pirata One', serif;
  font-weight: 400;
  color: #2e2010;
  letter-spacing: .3px;
}
.entity-detail-bio h2 { font-size: 22px; margin: 26px 0 10px; }
.entity-detail-bio h3 { font-size: 18px; margin: 20px 0 8px; }
.entity-detail-bio h2:first-child, .entity-detail-bio h3:first-child { margin-top: 0; }
.entity-detail-bio ul, .entity-detail-bio ol {
  margin: 0 0 14px;
  padding-left: 22px;
  line-height: 1.7;
}
.entity-detail-bio li { margin-bottom: 4px; }
.entity-detail-bio strong { color: #2e2010; }
.entity-detail-bio blockquote {
  margin: 0 0 14px;
  padding-left: 16px;
  border-left: 3px solid var(--gold);
  color: var(--muted);
  font-style: italic;
}

@media (max-width: 600px) {
  .entity-infobox { float: none; width: 100%; max-width: 260px; margin: 0 auto 20px; }
}

/* =====================================================================
   ACCOUNTS — the sidebar entry and login.html
   =====================================================================

   Two related pieces:

   .sidebar-account is INJECTED by js/auth.js, not written into any
   page's markup — that markup is duplicated across every shell page, so
   one copy in JS beats twelve in HTML. It's built to look like the
   "Navigation" and "Universe" sections above it, so the rules here only
   cover what differs: the separation from the nav links, and the signed-
   in state.

   .account-page is login.html's own content. It has the only real form
   on the site (everything else is a toolbar control), which is why the
   .btn/.account-field styles start here rather than being lifted out of
   somewhere existing. If a second form ever appears, promote them.
   ===================================================================== */

/* The rest of the account block (heading, admin, Privacy, Licence) sits
   at the end of the open menu, pushed to the right so it reads as its
   own thing rather than one more place to browse. The account link
   itself is not in it: js/auth.js puts it straight in the bar, so it
   stays on screen while the menu is closed. */
.sidebar-account {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 4px 14px;
  margin: 0 0 0 auto;
}

/* =====================================================================
   THE SIDEBAR'S SEARCH BOX  (js/search.js, search.html)

   Injected by js/search.js under the logo and above "Navigation", for
   the same reason .sidebar-account is injected: that markup is
   duplicated across every shell page, and one copy in JS beats
   seventeen in HTML.

   The suggestion list is absolutely positioned so it overlays the nav
   links rather than pushing them down — a list that shoves the whole
   sidebar about while you type is a list nobody can aim at. Its parent
   carries the position, not the sidebar, so it stays anchored to the
   field.
   ===================================================================== */

.sidebar-search { margin: 14px 0 4px; }

.sidebar-search-field { position: relative; }

.sidebar-search-input {
  width: 100%;
  box-sizing: border-box;
  font-family: inherit;
  font-size: 14px;
  color: var(--ink);
  background: rgba(255,255,255,.3);
  border: 1px solid var(--gold);
  border-radius: 2px;
  padding: 7px 9px;
}
.sidebar-search-input::placeholder { color: var(--muted); opacity: .85; }
.sidebar-search-input:focus {
  outline: none;
  border-color: #8a6a28;
  box-shadow: 0 0 0 2px rgba(201,154,62,.35);
}
/* The browser's own clear button, which is a system glyph that fights
   the parchment on every platform. The Escape key closes the list and
   selecting-all clears the text; nothing is lost. */
.sidebar-search-input::-webkit-search-cancel-button { display: none; }

.sidebar-search-results {
  position: absolute;
  top: calc(100% + 4px);
  left: 0;
  right: 0;
  z-index: 20;
  margin: 0;
  padding: 0;
  list-style: none;
  max-height: 320px;
  overflow-y: auto;
  background: var(--parchment-light);
  border: 1px solid var(--gold);
  box-shadow: 0 10px 24px rgba(0,0,0,.35);
}

.sidebar-search-option + .sidebar-search-option {
  border-top: 1px solid rgba(201,154,62,.4);
}

/* The whole row is the link, so the target is the row and not the word
   inside it — the .sidebar a rule above would otherwise make this an
   inline-sized hit area. */
.sidebar-search-option a {
  display: block;
  padding: 7px 9px;
  text-decoration: none;
}
.sidebar-search-option a:hover { text-decoration: none; }
.sidebar-search-option:hover,
.sidebar-search-option.is-active { background: rgba(201,154,62,.28); }

.sidebar-search-name {
  display: block;
  font-size: 14px;
  color: var(--link);
  overflow-wrap: anywhere;
}

.sidebar-search-kind {
  display: inline-block;
  margin-top: 2px;
  font-size: 10px;
  font-weight: 700;
  letter-spacing: .8px;
  text-transform: uppercase;
  color: var(--muted);
}

.sidebar-search-detail {
  display: block;
  font-size: 12px;
  color: var(--muted);
  overflow-wrap: anywhere;
}

.sidebar-search-empty {
  padding: 8px 9px;
  font-size: 13px;
  font-style: italic;
  color: var(--muted);
}

/* The result count, for a screen reader. Off-screen rather than
   display:none, because a hidden element is not announced at all. */
.sidebar-search-status {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* ---- search.html ---- */

.search-group { margin-top: 30px; }
.search-group:first-child { margin-top: 8px; }

.search-group-count {
  margin-left: 10px;
  font-family: system-ui, sans-serif;
  font-size: 12.5px;
  color: var(--muted);
}

.search-list { list-style: none; margin: 12px 0 0; padding: 0; }

.search-result {
  border: 1px solid var(--gold);
  background: var(--parchment-light);
  margin-bottom: 8px;
}
.search-result:last-child { margin-bottom: 0; }

.search-result-link { display: block; padding: 10px 14px; text-decoration: none; }
.search-result-link:hover { background: rgba(201,154,62,.16); }

.search-result-name {
  display: block;
  font-family: 'Pirata One', serif;
  font-weight: 400;
  font-size: 19px;
  letter-spacing: .3px;
  color: var(--link);
  overflow-wrap: anywhere;
}

.search-result-detail {
  display: block;
  margin-top: 2px;
  font-size: 13.5px;
  color: var(--muted);
  overflow-wrap: anywhere;
}

.sidebar-account-link.is-signed-in {
  font-weight: 600;
  /* The account's own name, not a destination: same colour as the
     headings above rather than the link blue used for navigation. */
  color: #4a3413;
  /* Long display names are cut with an ellipsis in the bar (see
     .sidebar > .sidebar-account-link); this covers anywhere else. */
  overflow-wrap: anywhere;
}

/* ---- login.html ---- */

.account-page { max-width: 620px; }

.account-subtitle {
  font-family: 'Pirata One', serif;
  font-weight: 400;
  font-size: 24px;
  color: #2e2010;
  letter-spacing: .3px;
  margin: 0 0 4px;
}

.account-form {
  display: flex;
  align-items: flex-end;
  flex-wrap: wrap;
  gap: 12px;
  margin: 22px 0 6px;
}

.account-field {
  display: flex;
  flex-direction: column;
  gap: 6px;
  font-size: 14px;
}
.account-field span { color: var(--muted); letter-spacing: .3px; }
.account-field input {
  border: 1px solid var(--gold);
  background: var(--parchment-light);
  color: var(--ink);
  padding: 9px 11px;
  font-family: 'EB Garamond', serif;
  font-size: 15px;
  width: 280px;
  max-width: 100%;
}
.account-field input:focus {
  outline: 2px solid var(--gold);
  outline-offset: 1px;
}

.btn {
  border: 1px solid var(--gold);
  background: var(--parchment-light);
  color: #2e2010;
  padding: 10px 18px;
  font-family: 'Pirata One', serif;
  font-size: 17px;
  letter-spacing: .5px;
  cursor: pointer;
  box-shadow: 0 2px 6px rgba(0,0,0,.25);
}
.btn:hover { background: #f0dcae; }
.btn:disabled { opacity: .6; cursor: default; box-shadow: none; }

/* Sign out: same shape, deliberately quieter — it sits under the
   account details and shouldn't be the loudest thing on the page. */
.btn-quiet {
  background: transparent;
  font-family: 'EB Garamond', serif;
  font-size: 15px;
  padding: 8px 14px;
  box-shadow: none;
}
.btn-quiet:hover { background: rgba(201,154,62,.18); }

.account-actions {
  margin-top: 28px;
  padding-top: 20px;
  border-top: 1px solid var(--gold);
}

.account-note { font-size: 14px; color: var(--muted); margin: 4px 0 0; }

/* The age statement on the sign-up form. A checkbox and its label on
   one line, which .account-field's stacked layout doesn't give. */
.account-check {
  display: flex;
  align-items: flex-start;
  gap: 9px;
  margin-top: 4px;
  font-size: 14px;
  line-height: 1.5;
}
.account-check input { margin-top: 2px; flex: 0 0 auto; }

/* The two blocks at the bottom of the account page — seeing your data
   and deleting the account. Both are folded away by default: they are
   things you do once, not things you scroll past every time you change
   a display name.

   Real <details>, like the diff and the admin panel's account groups:
   it folds with the keyboard, it is announced as expandable, and the
   browser keeps the state. Closed, the summary is the whole block, so
   the padding lives on the body rather than on the container — a closed
   fold should be a line, not a box with air in it. */
.account-fold { margin-top: 24px; }

.account-fold-summary {
  cursor: pointer;
  list-style: none;
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 12px 18px;
  font-family: 'Pirata One', serif;
  font-weight: 400;
  font-size: 20px;
  letter-spacing: .3px;
  color: #2e2010;
}
.account-fold-summary::-webkit-details-marker { display: none; }
.account-fold-summary::before {
  content: "";
  border: 5px solid transparent;
  border-left-color: var(--muted);
  border-right-width: 0;
  transition: transform .15s ease;
  flex: 0 0 auto;
}
.account-fold[open] > .account-fold-summary::before { transform: rotate(90deg); }

.account-fold-body { padding: 0 18px 18px; }

/* Seeing and downloading what the site holds about you. Quieter than
   the deletion block below it: this one only ever reads.

   Still a fold, deliberately — it is what keeps the summary lazy, since
   the page only queries anything when somebody opens it. The prose that
   used to be inside moved to privacy.html; the drawer stayed. */
.data-zone {
  border: 1px solid var(--gold);
  background: rgba(255,255,255,.18);
}

.data-zone .account-note { max-width: 640px; line-height: 1.6; }
.data-zone .btn { margin-top: 16px; }

.data-summary { margin-top: 14px; }

/* Two columns on a wide card, stacked on a narrow one — a definition
   list rather than a table because that is what this is. */
.data-list {
  display: grid;
  grid-template-columns: max-content 1fr;
  gap: 4px 18px;
  margin: 0;
  font-size: 14px;
}
.data-list dt { color: var(--muted); }
.data-list dd { margin: 0; overflow-wrap: anywhere; }

/* Deleting the account. Set apart from the rest of the account page
   because every other control there changes something you can change
   back. The red is a left border, never the text colour — same rule as
   the diff: it marks the block without making it harder to read for
   anyone who can't separate the hues. */
.danger-zone {
  border: 1px solid rgba(138,46,34,.4);
  border-left: 3px solid var(--red);
  background: rgba(138,46,34,.05);
}

.danger-zone .account-note { max-width: 640px; line-height: 1.6; }
.danger-zone .btn { margin-top: 14px; }
.danger-zone .change-reject-box { margin-top: 12px; }
.account-error { font-size: 14px; color: var(--red); margin: 10px 0 0; }

/* "Admin" next to the account name. Not a permission control — the
   database decides that (profiles.is_admin, set in Studio only) — just
   a visible confirmation of which account you're using. */
.account-badge {
  display: inline-block;
  margin-left: 10px;
  padding: 2px 9px;
  border: 1px solid var(--gold);
  border-radius: 2px;
  background: rgba(201,154,62,.22);
  font-family: 'EB Garamond', serif;
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 1px;
  text-transform: uppercase;
  color: #4a3413;
  vertical-align: middle;
}

/* The "create an account" / "forgot your password?" links under a form.
   Deliberately plain text links: they're alternatives to the form above,
   not competing actions, so nothing here should look like a button. */
.account-links { margin: 18px 0 0; font-size: 14px; }
.account-links-sep { color: var(--muted); margin: 0 8px; }

/* Forms with more than one field stack instead of flowing across the
   row: sign-in, sign-up and "choose a new password". Two 280px fields
   plus a button don't fit .account-page's 620px, so the default inline
   flow wrapped them into a ragged two-and-a-half-row shape. The
   single-field forms (a display name, an address) keep the inline
   layout, where the button beside the field reads as one unit. */
.account-form-stacked {
  flex-direction: column;
  align-items: flex-start;
  gap: 16px;
}

/* =====================================================================
   RECENT CHANGES / PATROL  (recent-changes.html, js/recent-changes.js)

   A log, not a gallery: everything here is read in a column, oldest
   convention on the site (the .card grids) deliberately not reused.
   Entries are nested parchment cards like .entity-card, one shade
   lighter than the page card they sit on, so a long list still reads as
   a stack of separate things.

   The one new colour idea is the before/after pair. It borrows --red
   (which had been reserved and unused until now) for the outgoing value
   and a matching muted green for the incoming one — as a LEFT BORDER and
   a tag word, never as the text colour, because a diff has to stay
   readable to anyone who can't tell the two hues apart. The word
   "before"/"after" is the actual signal; the colour only backs it up.
   ===================================================================== */

.changes-page { max-width: 760px; }

.changes-section { margin-top: 34px; }
.changes-section:first-of-type { margin-top: 26px; }

.changes-heading {
  font-family: 'Pirata One', serif;
  font-weight: 400;
  font-size: 24px;
  color: #2e2010;
  letter-spacing: .3px;
  margin: 0 0 4px;
  padding-bottom: 6px;
  border-bottom: 1px solid var(--gold);
}

.change-list { margin-top: 16px; }

.change-entry {
  background: var(--parchment-light);
  border: 1px solid var(--gold);
  padding: 14px 16px;
  margin-bottom: 14px;
}
.change-entry:last-child { margin-bottom: 0; }

/* Title on the left, status on the right, wrapping to two lines on a
   phone rather than squeezing the status into nothing. */
.change-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 6px 12px;
}

.change-title {
  font-family: 'Pirata One', serif;
  font-weight: 400;
  font-size: 20px;
  letter-spacing: .3px;
  color: #2e2010;
  margin: 0;
  /* An id or a name from an unreviewed submission can be anything at
     all, including one very long word. */
  overflow-wrap: anywhere;
}

/* "character", "creature", … after the name. */
.change-kind {
  margin-left: 9px;
  font-family: 'EB Garamond', serif;
  font-size: 13px;
  font-weight: 400;
  letter-spacing: .5px;
  text-transform: uppercase;
  color: var(--muted);
}

.change-status {
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 1px;
  text-transform: uppercase;
  padding: 2px 9px;
  border: 1px solid var(--gold);
  border-radius: 2px;
  white-space: nowrap;
}
.change-status-pending  { background: rgba(201,154,62,.28); color: #4a3413; }
.change-status-approved { background: rgba(63,107,58,.18);  color: #2f4f2b; border-color: #3f6b3a; }
.change-status-rejected { background: rgba(138,46,34,.14);  color: var(--red); border-color: var(--red); }

.change-meta {
  font-size: 13.5px;
  color: var(--muted);
  margin: 6px 0 0;
}
.change-meta strong { color: #4a3413; font-weight: 600; }
/* Carries a title attribute with the exact timestamp — the dotted
   underline is what says so. */
.change-time { border-bottom: 1px dotted transparent; cursor: default; }
.change-time[title]:not([title=""]) { border-bottom-color: rgba(107,90,66,.5); }

/* The role beside a direct edit — "Edit made by X · Trusted
   Contributor". Quiet: it explains why there was no review, it isn't a
   decoration on the person's name. */
.change-direct-role {
  margin-left: 8px;
  padding: 1px 7px;
  border: 1px solid var(--gold);
  border-radius: 2px;
  background: rgba(201,154,62,.18);
  color: #4a3413;
  font-size: 10.5px;
  font-weight: 700;
  letter-spacing: .8px;
  text-transform: uppercase;
  white-space: nowrap;
}

/* A change in the edits history with a signed-in author but no
   suggestion behind it — a restore, in practice. Deliberately duller
   than .change-direct-role beside it: it notes how the change arrived,
   it does not confer anything on the person. */
.change-outside {
  margin-left: 8px;
  padding: 1px 7px;
  border: 1px solid rgba(90,74,48,.35);
  border-radius: 2px;
  color: #6b5a3c;
  font-size: 10.5px;
  font-weight: 700;
  letter-spacing: .8px;
  text-transform: uppercase;
  white-space: nowrap;
}

.change-note {
  font-size: 14px;
  margin: 8px 0 0;
  padding: 8px 10px;
  background: rgba(255,255,255,.28);
  border-left: 3px solid var(--gold);
  overflow-wrap: anywhere;
}
.change-note-label { font-weight: 600; color: #4a3413; }

.change-warning {
  font-size: 14px;
  color: var(--red);
  margin: 8px 0 0;
}

/* ---- the expandable diff ---- */

.change-details { margin-top: 10px; }

.change-details > summary {
  cursor: pointer;
  font-size: 14px;
  color: var(--link);
  /* The default triangle is a browser-specific shape; this replaces it
     with the same caret the collapsible category headings use. */
  list-style: none;
  display: inline-flex;
  align-items: center;
  gap: 7px;
}
.change-details > summary::-webkit-details-marker { display: none; }
.change-details > summary::before {
  content: "";
  border: 5px solid transparent;
  border-left-color: var(--muted);
  border-right-width: 0;
  transition: transform .15s ease;
}
.change-details[open] > summary::before { transform: rotate(90deg); }

.change-fields { margin-top: 12px; }

.change-field { margin-bottom: 16px; }
.change-field:last-child { margin-bottom: 0; }

.change-field-name {
  font-family: 'EB Garamond', serif;
  font-size: 13px;
  font-weight: 700;
  letter-spacing: .6px;
  text-transform: uppercase;
  color: var(--muted);
  margin: 0 0 6px;
}

.change-value {
  border-left: 3px solid var(--muted);
  padding: 6px 10px;
  background: rgba(255,255,255,.24);
  margin-bottom: 6px;
}
.change-value:last-child { margin-bottom: 0; }
.change-value-before { border-left-color: var(--red); background: rgba(138,46,34,.07); }
.change-value-after  { border-left-color: #3f6b3a;    background: rgba(63,107,58,.09); }

.change-value-tag {
  display: block;
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 1px;
  text-transform: uppercase;
  color: var(--muted);
  margin-bottom: 3px;
}

/* Values are shown exactly as stored — Markdown source, JSON, a file
   path — so they keep their line breaks and never get re-wrapped into
   something that isn't what the contributor typed. Long ones scroll
   inside their own box rather than making the page taller than the
   list it belongs to. */
.change-value pre {
  margin: 0;
  font-family: ui-monospace, 'SFMono-Regular', Menlo, Consolas, monospace;
  font-size: 12.5px;
  line-height: 1.55;
  color: var(--ink);
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  max-height: 260px;
  overflow-y: auto;
}

/* ---- the line-by-line comparison ----

   Two columns of the same lines, only where they differ, with a couple
   of unchanged ones either side for bearings. Built by
   FLA.buildTextDiff in js/data.js and used by the patrol queue, the
   edits history and a page's own history.

   Colour is never the only signal: every changed line carries a − or a
   + beside its number, and the pair of columns is labelled in words
   above. Someone who can't separate the two tints still reads it. */

.diff-legend {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 10px;
  margin-bottom: 4px;
}
.diff-legend span {
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 1px;
  text-transform: uppercase;
  color: var(--muted);
}

.diff {
  border: 1px solid rgba(107,90,66,.3);
  background: rgba(255,255,255,.24);
  /* A very long unbroken line scrolls this box rather than the page. */
  max-height: 340px;
  overflow-y: auto;
}

.diff-row {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 10px;
}

.diff-cell {
  display: flex;
  align-items: baseline;
  gap: 8px;
  padding: 2px 8px;
  font-family: ui-monospace, 'SFMono-Regular', Menlo, Consolas, monospace;
  font-size: 12.5px;
  line-height: 1.55;
  color: var(--ink);
  min-width: 0;
}

.diff-lineno {
  flex: 0 0 auto;
  min-width: 2.2em;
  text-align: right;
  font-size: 11px;
  color: var(--muted);
  user-select: none;
}

.diff-marker {
  flex: 0 0 auto;
  font-weight: 700;
  user-select: none;
}

.diff-text {
  min-width: 0;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}

.diff-row-change .diff-cell-before {
  background: rgba(138,46,34,.09);
  box-shadow: inset 3px 0 0 var(--red);
}
.diff-row-change .diff-cell-after {
  background: rgba(63,107,58,.11);
  box-shadow: inset 3px 0 0 #3f6b3a;
}
.diff-row-change .diff-cell-before .diff-marker { color: var(--red); }
.diff-row-change .diff-cell-after .diff-marker { color: #2f5a2b; }

/* The empty half of a pure insertion or deletion. Hatched rather than
   blank so it reads as "nothing here" rather than "not loaded" — and
   after the two rules above, with a class from each, because otherwise
   they win on specificity and tint a cell that holds nothing. */
.diff-row-change .diff-cell-empty {
  background: repeating-linear-gradient(
    -45deg,
    transparent, transparent 5px,
    rgba(107,90,66,.09) 5px, rgba(107,90,66,.09) 10px);
  box-shadow: none;
}

/* The words that actually moved, inside a line that replaced another. */
.diff-word-del { background: rgba(138,46,34,.28); }
.diff-word-ins { background: rgba(63,107,58,.3); }

.diff-skip {
  padding: 3px 10px;
  border-top: 1px solid rgba(107,90,66,.22);
  border-bottom: 1px solid rgba(107,90,66,.22);
  background: rgba(107,90,66,.07);
  font-size: 11px;
  letter-spacing: .6px;
  text-transform: uppercase;
  color: var(--muted);
}

/* "contains HTML" beside a field name in the patrol queue. Not a
   warning that something is dangerous — the renderer sanitizes — but a
   note that this field is markup and should be read as such before it's
   approved. See hasMarkup() in js/recent-changes.js. */
.change-field-flag {
  margin-left: 8px;
  padding: 1px 7px;
  border: 1px solid var(--red);
  border-radius: 2px;
  background: rgba(138,46,34,.1);
  color: var(--red);
  font-size: 10.5px;
  font-weight: 700;
  letter-spacing: .8px;
  text-transform: uppercase;
}

.change-untouched { font-size: 13px; margin: 0; }

/* "changed here" beside a field name on a page's own history: this
   version is where that field became what it says. Quieter than the
   patrol queue's before/after, because a history is read as a sequence
   rather than as a decision to make. */
.change-field-changed {
  margin-left: 8px;
  padding: 1px 7px;
  border: 1px solid #3f6b3a;
  border-radius: 2px;
  background: rgba(63,107,58,.14);
  color: #2f4f2b;
  font-size: 10.5px;
  font-weight: 700;
  letter-spacing: .8px;
  text-transform: uppercase;
}

/* The live row, sitting on top of a page's history. Marked with a
   heavier border rather than a different colour: it's the same kind of
   thing as the entries below it, just the one that is current. */
.change-entry-current { border-width: 2px; }

/* "See this page's history", under an entry in the global feed. A plain
   link: restoring lives over there, and this is the way to it, not an
   action in its own right. */
.change-history-link {
  display: inline-block;
  margin-top: 10px;
  font-size: 13.5px;
}

/* ---- moderation ---- */

.change-actions {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 10px;
  margin-top: 14px;
  padding-top: 12px;
  border-top: 1px solid rgba(201,154,62,.55);
}

.change-reject-box {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 8px;
  /* Own row: the note field plus two buttons never fit beside the two
     buttons that opened it. */
  flex-basis: 100%;
}
.change-reject-note {
  border: 1px solid var(--gold);
  background: var(--parchment);
  color: var(--ink);
  padding: 8px 10px;
  font-family: 'EB Garamond', serif;
  font-size: 15px;
  flex: 1 1 240px;
  min-width: 0;
}
.change-reject-note:focus { outline: 2px solid var(--gold); outline-offset: 1px; }

/* The sentence in front of the two confirm buttons — "Replace the page
   as it is now with this version?". Sits on the same row, so restoring
   asks and answers in one place. */
.change-confirm-question {
  flex: 1 1 100%;
  font-size: 14px;
  color: var(--red);
}

.change-action-status {
  flex-basis: 100%;
  font-size: 14px;
  color: var(--muted);
  margin: 0;
}
.change-action-error { color: var(--red); }

.changes-more { margin-top: 16px; }

@media (max-width: 600px) {
  /* The status chip on its own line, left-aligned with the title, rather
     than pushed to a ragged right edge by justify-content. */
  .change-head { justify-content: flex-start; }
  .change-value pre { font-size: 12px; }

  /* The comparison stacks: two columns of monospace in 190px each is
     not a diff, it's a column of single words. The legend goes with
     them — it labels two columns that no longer exist — and the − / +
     markers carry the distinction on their own, which is why they were
     never decoration in the first place. An unchanged line appears
     once; the blank half of an insertion or deletion not at all. */
  .diff-legend { display: none; }
  .diff-row { grid-template-columns: 1fr; gap: 0; }
  .diff-row-same .diff-cell-after { display: none; }
  .diff-cell-empty { display: none; }
  .diff-cell { font-size: 12px; }
}

/* =====================================================================
   SUGGESTING AN EDIT  (suggest.html, js/suggest.js)

   The site's second real form, and the first one that isn't a handful of
   short fields — so it reuses .account-field/.btn from the accounts
   block above rather than growing a parallel set, and adds only what a
   long form actually needs: full-width fields, a hint line under each
   one, and the repeating infobox rows.

   (The accounts block says to promote .btn/.account-field out of it if a
   second form ever appears. This is that second form; they're being
   shared in place instead, because moving them is a bigger edit than it
   looks — every rule in both blocks would need re-reading — and nothing
   about them is account-specific any more than it is form-specific.)

   The "Suggest an edit" button that leads here (.suggest-bar) is at the
   end of this block: it's the same control on nine other pages, built by
   FLA.pageActions() in js/data.js.
   ===================================================================== */

.suggest-page { max-width: 820px; }

.suggest-form {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: 20px;
  margin: 24px 0 6px;
}

/* .account-field caps its input at 280px, which suits an email address
   and not a character's name. Here every field runs the width of the
   form and the long ones get room to breathe. */
.suggest-field { width: 100%; }
.suggest-field input { width: 100%; }
.suggest-field textarea {
  border: 1px solid var(--gold);
  background: var(--parchment-light);
  color: var(--ink);
  padding: 10px 12px;
  /* Monospace, because it's Markdown source: alignment of list markers
     and heading hashes is part of reading it. */
  font-family: ui-monospace, 'SFMono-Regular', Menlo, Consolas, monospace;
  font-size: 13.5px;
  line-height: 1.6;
  width: 100%;
  resize: vertical;
}
.suggest-field textarea:focus { outline: 2px solid var(--gold); outline-offset: 1px; }

/* The write-up and infobox blocks are <div>s, not <label>s — a <label>
   wrapping a control means clicking anywhere in it focuses that control,
   which is wrong once the block contains its own buttons. So they need
   the field label styled by hand. */
.suggest-field-label {
  color: var(--muted);
  font-size: 14px;
  letter-spacing: .3px;
}

.suggest-hint {
  font-size: 13px;
  color: var(--muted);
  line-height: 1.5;
}
.suggest-hint code {
  font-family: ui-monospace, 'SFMono-Regular', Menlo, Consolas, monospace;
  font-size: 12.5px;
  color: #4a3413;
}

/* A button that reads as a link: the preview toggle sits mid-sentence in
   a hint line, where a real button would shout. */
.btn-link {
  border: 0;
  background: none;
  padding: 0;
  font: inherit;
  color: var(--link);
  text-decoration: underline;
  cursor: pointer;
}

/* The rendered preview stands in for the textarea, so it occupies the
   same visual slot — bordered like the field it replaces, and carrying
   .entity-detail-bio as well so the Markdown renders exactly as it will
   on the real page. */
.suggest-preview {
  border: 1px solid var(--gold);
  background: rgba(255,255,255,.28);
  padding: 12px 14px;
  min-height: 120px;
}
.suggest-preview > :first-child { margin-top: 0; }
.suggest-preview > :last-child { margin-bottom: 0; }

/* ---- the infobox row editor ---- */

.infobox-rows { display: flex; flex-direction: column; gap: 8px; }

.infobox-row {
  display: flex;
  align-items: center;
  gap: 8px;
}
/* Label narrower than value: labels are one or two words ("Species",
   "Affiliation"), values are often a list. */
.infobox-row .infobox-label { flex: 1 1 30%; min-width: 0; }
.infobox-row .infobox-value { flex: 1 1 70%; min-width: 0; }
.infobox-row input {
  border: 1px solid var(--gold);
  background: var(--parchment-light);
  color: var(--ink);
  padding: 8px 10px;
  font-family: 'EB Garamond', serif;
  font-size: 15px;
}
.infobox-row input:focus { outline: 2px solid var(--gold); outline-offset: 1px; }

.infobox-row-tools { display: flex; gap: 4px; flex: 0 0 auto; }
.infobox-row-btn {
  border: 1px solid var(--gold);
  background: transparent;
  color: #4a3413;
  width: 30px;
  height: 32px;
  font-size: 14px;
  line-height: 1;
  cursor: pointer;
}
.infobox-row-btn:hover { background: rgba(201,154,62,.22); }

.infobox-tools { margin-top: 10px; }

/* ---- Drafts ----
   An entry with published = false. Only ever on screen for an admin —
   every other visitor's query filtered it out — so neither rule below is
   a permission boundary. They are labels, and their whole job is that a
   draft can never be mistaken for a live page.

   Warm amber, not the site's --red, which is reserved for removal and
   error. A draft is not wrong; it is not out yet. And the word
   "Unpublished" carries the meaning on its own, per the standing rule
   that no state rests on a colour alone. */
.draft-banner {
  margin: 0 0 18px;
  padding: 10px 14px;
  border: 1px solid #b8862f;
  border-left-width: 4px;
  background: rgba(184, 134, 47, .12);
  color: #5a4218;
  font-size: 14px;
  line-height: 1.5;
}
.draft-banner strong {
  text-transform: uppercase;
  letter-spacing: .6px;
  font-size: 12.5px;
}

/* On the reader, which is dark and has no parchment card to sit on. Full
   width under the header rather than floating over the page, so it never
   covers the artwork it is describing. */
.draft-banner-reader {
  margin: 0;
  border: 0;
  background: rgba(184, 134, 47, .22);
  color: #f4e2bf;
  text-align: center;
  font-size: 13px;
  padding: 8px 14px;
}

/* The gallery counterpart: a corner tag on a draft's card. Absolutely
   positioned so it cannot change a card's height and leave the grid
   ragged — these are fixed-ratio grids and one taller cell would show.
   The card is only made a positioning context when it is a draft, so
   nothing about an ordinary card changes. */
.is-draft { position: relative; }
.draft-tag {
  position: absolute;
  top: 8px;
  left: 8px;
  background: #b8862f;
  color: #241a08;
  font-size: 10.5px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: .6px;
  padding: 3px 7px;
  border-radius: 2px;
  pointer-events: none;
}

/* ---- [[wiki links]] ----
   A link written inside a write-up. A resolved one looks like every
   other link on the site — the class exists for the unresolved ones.

   Red, per the site's own rule, is never the ONLY signal: the dotted
   underline distinguishes a missing link from a live one for anyone who
   does not separate the two colours, and the tooltip says it in words. */
.wikilink-missing {
  color: var(--red);
  border-bottom: 1px dotted currentColor;
  cursor: help;
}
/* The clickable half: a red link that named a type goes straight to the
   form that would create the page — how a wiki asks to be written — so
   it gets a pointer instead of the help cursor. One without a type is a
   <span>, and keeps the help cursor and its explanation. */
a.wikilink-missing { cursor: pointer; text-decoration: none; }
a.wikilink-missing:hover { border-bottom-style: solid; }

/* ---- Redirects ----
   "Redirected from old-address", on a page reached through an alias.
   Wiki-style: the address in the bar is still the one that was asked
   for, so without this line the reader would be looking at a page whose
   name does not match the link they followed, with nothing to explain
   it. Quiet — a footnote about how you got here, not news. */
.redirect-note {
  margin: 0 0 16px;
  font-size: 13px;
  color: var(--muted);
  font-style: italic;
}
.redirect-note code {
  font-family: ui-monospace, 'SFMono-Regular', Menlo, Consolas, monospace;
  font-size: 12.5px;
  font-style: normal;
  color: #4a3413;
}

/* On the reader's dark header, beneath .draft-banner-reader when both
   are there. */
.redirect-note-reader {
  margin: 0;
  padding: 6px 14px;
  text-align: center;
  background: rgba(255, 255, 255, .06);
  color: rgba(255, 255, 255, .66);
  font-size: 12px;
}
.redirect-note-reader code { color: rgba(255, 255, 255, .85); }

/* The artwork credit under the comic library's heading. The parchment
   counterpart of .reader-credit in css/reader.css, which does the same
   job on the reader's dark header — the two sentences have to keep
   agreeing, so if one changes, find the other. */
.library-credit {
  font-size: 13px;
  color: var(--muted);
  margin: 4px 0 0;
  line-height: 1.5;
}

/* The licence grant above the submit button. Quiet like .suggest-hint —
   it is a term, not a warning — but NOT hidden and not folded: it is the
   thing the button acts on, and it sits above the rule .suggest-actions
   draws so that the button and the sentence read as one unit. */
.suggest-licence {
  font-size: 13px;
  color: var(--muted);
  line-height: 1.55;
  margin: 26px 0 0;
  max-width: 620px;
}

.suggest-actions {
  display: flex;
  align-items: center;
  gap: 12px;
  margin-top: 8px;
  padding-top: 20px;
  border-top: 1px solid var(--gold);
}

/* ---- the entry point, on nine other pages ---- */

/* Right-aligned above the content it applies to, so it reads as an
   action on the page rather than part of the page. Deliberately a real
   button and not a quiet link: Ambre's call was that suggesting an edit
   should be findable, unlike the sign-in entry, which stays discreet. */
.suggest-bar {
  display: flex;
  justify-content: flex-end;
  margin-bottom: 16px;
}
/* .btn was written for <button>; this one is an <a>, so it also has to
   undo the site-wide link colour and underline. */
.btn-suggest {
  font-size: 15px;
  padding: 8px 16px;
  color: #2e2010;
  text-decoration: none;
  display: inline-block;
}
.btn-suggest:hover { text-decoration: none; }

@media (max-width: 600px) {
  /* Three tiny buttons plus two inputs don't fit a phone row, so a row
     becomes three stacked lines — which makes it much less obvious where
     one stat ends and the next begins. Hence the rule between them. */
  .infobox-row {
    flex-wrap: wrap;
    padding-bottom: 10px;
    border-bottom: 1px solid rgba(201,154,62,.5);
  }
  .infobox-row:last-child { border-bottom: 0; padding-bottom: 0; }
  .infobox-row .infobox-label { flex: 1 1 100%; }
  .infobox-row .infobox-value { flex: 1 1 100%; }
  .infobox-rows { gap: 12px; }
  .suggest-bar { justify-content: stretch; }
  .btn-suggest { width: 100%; text-align: center; }
}

/* =====================================================================
   CONTRIBUTORS  (member.html, the admin panel's account list, and the
   avatar+name chip that credits an edit wherever one appears)

   The chip is the piece that shows up most: a small round picture and a
   name, inline in a sentence like "Edit suggested by ___". It has to sit
   on a text baseline without pushing the line height around, which is
   why the avatar is sized in em rather than px — it tracks whatever text
   it lands in.
   ===================================================================== */

.member-chip {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  vertical-align: baseline;
  text-decoration: none;
  color: #4a3413;
  font-weight: 600;
}
a.member-chip:hover .member-chip-name { text-decoration: underline; }

/* An account somebody deleted. Not a link — there is no page to go to,
   and the point of the sentinel is that it leads nowhere and identifies
   nobody. Italic and muted so it reads as an absence rather than as a
   name; every deleted account looks the same because, as far as the
   archive is now concerned, they are. */
.member-chip-deleted .member-chip-name {
  font-weight: 400;
  font-style: italic;
  color: var(--muted);
}

.member-chip-avatar {
  width: 1.5em;
  height: 1.5em;
  border-radius: 50%;
  object-fit: cover;
  border: 1px solid var(--gold);
  /* Nudged onto the text baseline: a circle centred on the line box
     otherwise reads as sitting slightly high next to lowercase text. */
  vertical-align: -0.35em;
  flex: 0 0 auto;
}

.member-chip-name { overflow-wrap: anywhere; }

/* ---- member.html ---- */

.member-header {
  display: flex;
  align-items: flex-start;
  gap: 24px;
  padding-bottom: 22px;
  border-bottom: 1px solid var(--gold);
  margin-bottom: 8px;
}
.member-header-text { min-width: 0; }
.member-header h1 {
  font-family: 'Pirata One', serif;
  font-weight: 400;
  font-size: 34px;
  color: #2e2010;
  letter-spacing: .5px;
  margin: 8px 0 6px;
  overflow-wrap: anywhere;
}

.member-avatar {
  width: 120px;
  height: 120px;
  border-radius: 50%;
  object-fit: cover;
  border: 2px solid var(--gold);
  box-shadow: 0 4px 12px rgba(0,0,0,.3);
  flex: 0 0 auto;
}

/* Plain text somebody wrote about themselves — never Markdown, never
   HTML. Italic to set it apart from the archive's own prose, which is
   the site's voice rather than a contributor's. */
.member-bio {
  font-size: 16px;
  font-style: italic;
  line-height: 1.6;
  margin: 0 0 8px;
  max-width: 620px;
  overflow-wrap: anywhere;
}

/* ---- the admin panel's account list ---- */

.account-row {
  background: var(--parchment-light);
  border: 1px solid var(--gold);
  padding: 14px 16px;
  margin-bottom: 12px;
}
.account-row:last-child { margin-bottom: 0; }

.account-row-identity { display: flex; align-items: flex-start; gap: 14px; }
.account-row-text { min-width: 0; flex: 1; }

.account-row-avatar {
  width: 52px;
  height: 52px;
  border-radius: 50%;
  object-fit: cover;
  border: 1px solid var(--gold);
  flex: 0 0 auto;
}

.account-row-name {
  font-family: 'Pirata One', serif;
  font-weight: 400;
  font-size: 20px;
  letter-spacing: .3px;
  color: #2e2010;
  margin: 0 0 4px;
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 6px 10px;
  overflow-wrap: anywhere;
}

.account-row-meta { font-size: 13.5px; color: var(--muted); margin: 2px 0 0; }
.account-row-bio {
  font-size: 14px;
  font-style: italic;
  margin: 8px 0 0;
  overflow-wrap: anywhere;
}

/* ---- grouping and filtering that list ----

   Both are in-memory operations on a list the page already holds, so
   there is no loading state to design for: pressing a chip repaints. */

.account-filter { margin: 12px 0 18px; }

.account-filter-inner {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}

.account-filter-chip {
  font-family: inherit;
  font-size: 12.5px;
  font-weight: 700;
  letter-spacing: .5px;
  text-transform: uppercase;
  color: #4a3413;
  background: transparent;
  border: 1px solid var(--gold);
  border-radius: 2px;
  padding: 5px 11px;
  cursor: pointer;
  display: inline-flex;
  align-items: baseline;
  gap: 8px;
}
.account-filter-chip:hover:not(:disabled) { background: rgba(201,154,62,.14); }
/* The pressed one is filled and darker-edged. Not colour alone: the
   border weight carries it too, for the same reason the diff's before/
   after blocks are tagged in words rather than tinted and left at that. */
.account-filter-chip[aria-pressed="true"] {
  background: rgba(201,154,62,.32);
  border-color: #8a6a28;
}
.account-filter-chip:disabled { opacity: .45; cursor: default; }

.account-filter-count { font-size: 12px; font-weight: 400; color: var(--muted); }
.account-filter-chip[aria-pressed="true"] .account-filter-count { color: #4a3413; }

.account-group { margin-bottom: 20px; }
.account-group:last-child { margin-bottom: 0; }

.account-group-summary {
  cursor: pointer;
  /* Replaces the browser's own triangle with the caret used by the
     expandable diff and the collapsible category headings. */
  list-style: none;
  display: flex;
  align-items: baseline;
  gap: 10px;
  padding-bottom: 5px;
  border-bottom: 1px solid rgba(201,154,62,.55);
}
.account-group-summary::-webkit-details-marker { display: none; }
.account-group-summary::before {
  content: "";
  border: 5px solid transparent;
  border-left-color: var(--muted);
  border-right-width: 0;
  transition: transform .15s ease;
  align-self: center;
}
.account-group[open] > .account-group-summary::before { transform: rotate(90deg); }

.account-group-name {
  font-family: 'Pirata One', serif;
  font-weight: 400;
  font-size: 19px;
  letter-spacing: .3px;
  color: #2e2010;
}
.account-group-count { font-size: 13px; color: var(--muted); }

.account-group-body { margin-top: 12px; }

/* ---- the avatar editor on login.html ---- */

.avatar-field { margin-top: 22px; }

.avatar-editor {
  display: flex;
  align-items: center;
  gap: 16px;
  margin-top: 8px;
}

.avatar-preview {
  width: 96px;
  height: 96px;
  border-radius: 50%;
  object-fit: cover;
  border: 2px solid var(--gold);
}

.avatar-empty {
  width: 96px;
  height: 96px;
  border-radius: 50%;
  border: 1px dashed var(--gold);
  display: flex;
  align-items: center;
  justify-content: center;
  text-align: center;
  font-size: 12.5px;
  margin: 0;
  padding: 8px;
}

.avatar-controls { display: flex; flex-direction: column; gap: 8px; align-items: flex-start; }

/* A <label> wrapping a hidden file input: the only way to style the file
   picker at all, and it has to look like the button beside it. */
.avatar-pick { cursor: pointer; }

/* ---- the sidebar's admin and privacy entries ---- */

.sidebar-admin-link { font-size: 14px; }

/* Quieter than the links beside it: it is a reference, not a place to
   go. This is the site's footer — there is no
   other one, because the shell is duplicated by hand in every file and a
   real footer would be thirteen copies. */
.sidebar-legal-link {
  font-size: 12.5px;
  color: var(--muted);
}

@media (max-width: 600px) {
  .member-header { flex-direction: column; gap: 14px; }
  .member-avatar { width: 88px; height: 88px; }
  .account-row-identity { flex-direction: column; }
  /* The label beside each value goes above it: two columns of text in
     what's left of a phone screen is not a table, it's a stack of
     hyphens. */
  .data-list { grid-template-columns: 1fr; gap: 0 0; }
  .data-list dd { margin-bottom: 8px; }
}

/* =====================================================================
   Privacy notice (privacy.html)
   =====================================================================

   Long-form prose, which nothing else on this site is: a write-up runs
   through the Markdown renderer and gets .entity-detail-bio's rules, but
   this page is hand-written HTML. So it needs its own measure, its own
   heading rhythm and its own list spacing, and it gets them here rather
   than by borrowing a class that means something else.

   The measure is capped, unlike a write-up's, which deliberately has no
   max-width so it can wrap around a floated infobox. There is no float
   here, and 1200px of legal prose is unreadable. */

.privacy-page { max-width: 760px; }

.privacy-page h2 {
  font-family: 'Pirata One', 'Papyrus', fantasy;
  font-size: 26px;
  font-weight: normal;
  margin: 0 0 10px;
}

.privacy-page h3 {
  font-size: 16px;
  font-weight: bold;
  margin: 18px 0 6px;
}

.privacy-page p,
.privacy-page li { line-height: 1.65; }

.privacy-page ul {
  margin: 10px 0 0;
  padding-left: 22px;
}
.privacy-page li { margin-bottom: 8px; }

.privacy-section { margin-top: 34px; }

/* The box at the top, for the majority who read one thing and leave.
   Marked out from the sections below it so it reads as the answer
   rather than as the first of ten sections. */
.privacy-summary {
  border: 1px solid var(--gold);
  background: rgba(255,255,255,.18);
  border-radius: 4px;
  padding: 16px 18px 18px;
  margin-top: 26px;
}
.privacy-summary ul { margin-bottom: 0; }

@media (max-width: 600px) {
  .privacy-page h2 { font-size: 22px; }
}

/* =====================================================================
   Error pages (404.html, 50x.html)
   =====================================================================

   404.html reuses the ordinary two-column shell and needs almost nothing
   of its own — only the list and the closing note below. 50x.html is the
   odd one: no sidebar, no JavaScript, one centred card. See the comment
   at the top of that file for why the two differ. */

.error-links {
  margin: 0;
  padding-left: 22px;
  max-width: 680px;
}
.error-links li {
  line-height: 1.8;
  margin-bottom: 10px;
}

.error-note {
  margin-top: 26px;
  max-width: 680px;
  line-height: 1.8;
  color: var(--muted);
}

/* ---- 50x.html only ---- */

/* Stands in for .stage, which is a two-column grid this page has no
   second column for. Centred rather than left-aligned: with nothing
   beside it, a card pinned to the left of a wide screen looks like the
   sidebar failed to load — which is exactly the impression this page
   must not give. */
.error-stage {
  width: min(680px, 92%);
  margin: 12vh auto 34px;
}

.error-card { text-align: center; }

/* The logo carries the identity here, since there is no sidebar holding
   it. Bigger than the sidebar's 150px because it is the only thing above
   the fold. */
.error-card .logo { display: block; margin-bottom: 26px; }
.error-card .logo img { width: 190px; }

.error-title {
  font-family: 'Pirata One', 'Papyrus', fantasy;
  font-weight: 400;
  font-size: 40px;
  margin: 10px 0 22px;
  color: #2e2010;
  letter-spacing: .5px;
}

.error-body {
  line-height: 1.8;
  font-size: 17px;
  margin: 0 auto 16px;
  max-width: 520px;
}

@media (max-width: 600px) {
  .error-stage { margin-top: 6vh; }
  .error-title { font-size: 30px; }
  .error-card .logo img { width: 150px; }
}

/* =====================================================================
   Geography: a place's page (region.html)
   =====================================================================

   Only the two things a place has that no other entity does. The infobox,
   the write-up and everything else come from the shared "Generic entity
   system" block above — see js/place-extras.js for why these two live
   outside the shared renderer. */

/* Where this place sits: Familiar Lands / Great Europa / Franceaux.
   Styled like the .eyebrow that heads every other page, because it does
   the same job — it says where you are before you read anything. */
.place-trail {
  color: #8a5a1e;
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 2px;
  text-transform: uppercase;
  margin: 0 0 14px;
}
.place-trail a { text-decoration: none; }
.place-trail a:hover { text-decoration: underline; }
.place-trail-here { color: var(--muted); }

/* The line under a map's title: its own description, lifted out of the
   artwork. The world's era used to sit beneath it as a second, quieter
   line — that went when the painted words "Golden Age" on the world map
   became a link and the header started saying it twice. */
.map-description {
  max-width: 680px;
  margin: 0 0 10px;
  font-size: 15px;
  line-height: 1.6;
}


/* A place with a map of its own is a doorway, not just an article. */
.place-map-link { margin-top: 28px; }

.place-children { margin-top: 38px; }

.place-child-list {
  margin: 10px 0 0;
  padding-left: 22px;
  columns: 2;
  column-gap: 40px;
}
.place-child-list li {
  line-height: 1.7;
  margin-bottom: 6px;
  break-inside: avoid;
}
.place-child-note { color: var(--muted); }

/* Great Europa has some forty-five children and the U.S.L about fifty.
   Two columns on a wide card, one on a narrow one — a fifty-item list in
   a single column is a scroll, not a list. */
@media (max-width: 700px) {
  .place-child-list { columns: 1; }
}

/* ---- region.html: "Notable people" ----

   The people linked to this place, in the character gallery's own cards
   (js/place-extras.js reuses createCharacterCard() from
   js/characters.js), one grid per role under a small heading. The cards
   are narrower than on characters.html: this is a section of an article
   rather than the page's subject, and a place with two people should not
   hand them a row sized for twelve. */
.place-people { margin-top: 38px; }

.place-people-role {
  font-family: 'Pirata One', serif;
  font-weight: 400;
  font-size: 19px;
  color: var(--muted);
  margin: 22px 0 12px;
  letter-spacing: .5px;
}
.place-people-role:first-of-type { margin-top: 10px; }

.place-people-grid {
  grid-template-columns: repeat(auto-fill, minmax(128px, 1fr));
  gap: 14px;
}

/* =====================================================================
   Geography: the maps (geography.html)
   =====================================================================

   The illustration is the artwork; everything here is scaffolding around
   it and must not compete with it. Outlines are invisible until you point
   at one — a map pre-drawn with twenty coloured overlays would be a
   diagram, and Ambre drew a map. */

.map-page { max-width: none; }

.map-toolbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 14px;
  margin-bottom: 14px;
}

.map-layers, .map-zoom { display: flex; align-items: center; gap: 8px; }
.map-layers-label {
  color: #8a5a1e;
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 2px;
  text-transform: uppercase;
}
.map-layer-button {
  font: inherit;
  font-size: 14px;
  padding: 5px 12px;
  cursor: pointer;
  color: var(--ink);
  background: rgba(255,255,255,.25);
  border: 1px solid var(--gold);
  border-radius: 3px;
}
.map-layer-button[aria-pressed="true"] {
  background: var(--gold);
  cursor: default;
}

.map-frame { position: relative; }

/* The viewport. `touch-action: none` is what lets the pointer handlers
   own dragging and pinching — without it the browser scrolls the page
   instead and the map barely moves on a phone. */
.map-stage {
  position: relative;
  overflow: hidden;
  height: min(72vh, 760px);
  background: rgba(20,14,8,.18);
  border: 1px solid var(--gold);
  border-radius: 4px;
  touch-action: none;
  cursor: grab;
}
.map-stage.is-dragging { cursor: grabbing; }
.map-stage.is-imageless { background: rgba(20,14,8,.06); }

/* What actually moves. transform-origin at the corner so the arithmetic
   in zoomAt() is the arithmetic the browser does. */
.map-canvas {
  position: absolute;
  top: 0; left: 0;
  transform-origin: 0 0;
  will-change: transform;
}
.map-image { display: block; max-width: none; user-select: none; }

.map-shapes {
  position: absolute;
  top: 0; left: 0;
  width: 100%; height: 100%;
  overflow: visible;
}

/* Transparent until pointed at. The hairline is divided by the zoom in
   JS, otherwise a border becomes a fat band at 8x. */
.map-shape {
  fill: transparent;
  stroke: transparent;
  stroke-width: var(--map-hairline, 1.5);
  vector-effect: non-scaling-stroke;
  transition: fill .12s ease, stroke .12s ease;
}
.map-shapes a { cursor: pointer; outline: none; }
.map-shapes a:hover .map-shape,
.map-shapes a:focus-visible .map-shape {
  fill: rgba(255,255,255,.30);
  stroke: #fff;
}
.map-shapes a.is-selected .map-shape {
  fill: rgba(255,255,255,.38);
  stroke: #fff;
}
/* Keyboard focus has to be visible on an illustration of any colour, so
   it is a dashed dark stroke over the white one rather than a colour. */
.map-shapes a:focus-visible .map-shape {
  stroke-dasharray: 6 4;
  stroke: #140e08;
}
/* An outline with no page behind it yet — the same idea as an
   unpublished character in the reader's Related sidebar. */
.map-shapes a.is-unwritten .map-shape { stroke-dasharray: 3 5; }
.map-shapes a.is-unwritten:hover .map-shape { fill: rgba(255,255,255,.14); }

/* ---- words painted into the illustration that lead somewhere ----

   The world map's speech bubble names the era, and those two gold words
   link to the lore entry. Their resting cue is a rule UNDER the letters,
   never a wash over them: anything laid on top lightens hand-painted
   lettering, and an underline is what says "link" without touching a
   letterform. Both marks use non-scaling-stroke arithmetic in the same
   spirit as the hairline — the map runs from 0.33x to 8x, so a rule
   drawn naively would be invisible fitted and finger-thick zoomed in.

   The hover is deliberately quieter than a country's. On an 800px shape
   a white wash is fine; on two words it erases the gold. So: a warm
   cream wash at low alpha and the rule thickens and brightens. */
.map-link-hit {
  fill: transparent;
  stroke: none;
}
.map-link-rule {
  fill: rgba(150,112,40,.92);
  stroke: none;
}
.map-shapes a.map-link { cursor: pointer; }
.map-shapes a.map-link:hover .map-link-hit,
.map-shapes a.map-link:focus-visible .map-link-hit {
  /* .24 measured against the real artwork, not guessed: .36 washed the
     gold out of the lettering and .16 was barely perceptible. */
  fill: rgba(255,238,196,.24);
}
.map-shapes a.map-link:hover .map-link-rule,
.map-shapes a.map-link:focus-visible .map-link-rule {
  fill: rgba(190,145,50,1);
  transform: scaleY(2);
  transform-box: fill-box;
  transform-origin: bottom;
}
/* Keyboard focus has to be visible over any part of an illustration, so
   it is a dark dashed edge rather than a colour — the same answer the
   territories use. */
.map-shapes a.map-link:focus-visible .map-link-hit {
  stroke: #140e08;
  stroke-width: var(--map-hairline, 1.5);
  stroke-dasharray: 6 4;
}

/* Unchecked in the key: a dark wash laid over the country.

   DARKENED, NOT FADED, and the difference is what it says. Fading a
   country toward the parchment makes it look as though it failed to
   load; putting it in shadow reads as somebody having deliberately set
   it aside, which is what unchecking a level means. A fill rather than
   an opacity, for the same reason — opacity would take the artwork's own
   colour away, and the reader still wants to see which country it is.

   It stays clickable and stays in the layout. Shadow is a treatment, not
   a removal: a map with holes punched in it looks broken rather than
   filtered. */
.map-shapes a.is-muted .map-shape {
  fill: rgba(16,11,6,.66);
  stroke: rgba(16,11,6,.5);
}
/* Hover and selection still have to win over the wash, or a country you
   have set aside becomes one you cannot inspect. */
.map-shapes a.is-muted:hover .map-shape,
.map-shapes a.is-muted:focus-visible .map-shape,
.map-shapes a.is-muted.is-selected .map-shape {
  fill: rgba(90,74,52,.55);
  stroke: #fff;
}

.map-zoom .btn { min-width: 44px; }

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

.map-panel {
  position: absolute;
  top: 14px; right: 14px;
  width: min(330px, calc(100% - 28px));
  padding: 18px 20px 20px;
  background: var(--parchment-light);
  border: 2px solid var(--gold);
  border-radius: 4px;
  box-shadow: 0 18px 46px #0007;
}
.map-panel-close {
  position: absolute;
  top: 6px; right: 10px;
  border: 0; background: none;
  font-size: 24px; line-height: 1;
  cursor: pointer;
  color: var(--muted);
}
.map-panel-title {
  font-family: 'Pirata One', 'Papyrus', fantasy;
  font-weight: 400;
  font-size: 26px;
  margin: 0 0 4px;
  color: #2e2010;
}
/* The place's own one-line description — `geography.title`. It is body
   text rather than the small muted subtitle it used to be: the danger
   line below it is a classification, and this is the only line in the
   panel that says what the place actually IS. */
.map-panel-subtitle {
  margin: 0 0 12px;
  color: var(--ink);
  font-size: 15px;
  line-height: 1.5;
}
.map-panel-danger {
  display: flex;
  align-items: flex-start;
  gap: 8px;
  font-size: 13.5px;
  line-height: 1.5;
  margin: 0 0 16px;
}
/* "From here  Alexandra Athana" — names only, no portraits. The panel is
   a narrow column pinned beside the map; region.html is where the same
   links get cards. The role reads as a label, so it is set small and
   quiet rather than as a heading, which would out-shout the place's own
   title two lines above.

   SPECIFICITY TRAP, and it has already bitten once in this block:
   `.map-panel p` below is (0,1,1) and beats any bare single class, so a
   `.map-panel-people { margin }` would be silently overridden no matter
   where it sat in the file. That is exactly what has happened to
   .map-panel-danger's 16px — it renders at 12px. Hence the two-class
   selector here. Do not "simplify" it. */
.map-panel .map-panel-people {
  font-size: 13.5px;
  line-height: 1.6;
  margin: 0 0 8px;
}
.map-panel-people-role {
  display: block;
  color: var(--muted);
  font-size: 11.5px;
  text-transform: uppercase;
  letter-spacing: .6px;
}

.map-panel p { margin: 0 0 12px; }
.map-panel p:last-child { margin-bottom: 0; }

/* ---- the key: the legend, which is also the filter ----

   An overlay pinned inside the frame, where the illustration's own
   "Danger Level" cartouche used to be. It is positioned against
   .map-stage rather than .map-canvas so the pan-and-zoom transform
   doesn't carry it off the screen — see the comment in geography.html.

   It needs a real parchment backing, not a tint: it sits over dark
   ocean in one place and pale desert in another, and a translucent card
   would be legible over exactly one of them. */

.map-key {
  position: absolute;
  right: 14px;
  bottom: 14px;
  z-index: 2;
  max-width: min(300px, calc(100% - 28px));
  padding: 10px 12px;
  background: var(--parchment-light);
  border: 1px solid var(--gold);
  border-radius: 3px;
  box-shadow: 0 2px 10px rgba(20,14,8,.35);
}
.map-key-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
}
.map-key-title {
  font-family: 'Pirata One', 'Papyrus', fantasy;
  font-weight: 400;
  font-size: 19px;
  margin: 0;
  color: #2e2010;
}
.map-key-fold {
  font: inherit;
  font-size: 13px;
  line-height: 1;
  padding: 4px 6px;
  cursor: pointer;
  color: var(--ink);
  background: none;
  border: 1px solid transparent;
  border-radius: 3px;
}
.map-key-fold:hover { background: rgba(255,255,255,.35); }

.map-key-list { list-style: none; margin: 8px 0 0; padding: 0; }
.map-key-list li { margin-bottom: 2px; }
.map-key-list li:last-child { margin-bottom: 0; }

/* FOLDED: the swatches stay, and stay clickable. Only the words go.
   The title and the list turn into one compact row of dots, so the
   colours can still be read off the map and any level can still be
   switched off without unfolding anything. A fold that took the filter
   with it would be a worse control than no fold at all. */
.map-key.is-folded { padding: 8px 10px; }
.map-key.is-folded .map-key-title { display: none; }
.map-key.is-folded .map-key-list {
  display: flex;
  gap: 4px;
  margin: 0 8px 0 0;
}
.map-key.is-folded .map-key-head { flex-direction: row-reverse; }
.map-key.is-folded .map-key-list li { margin: 0; }
.map-key.is-folded .map-legend-item { padding: 4px; }
/* Off-screen, not display:none — the row keeps its accessible name, so
   a screen reader still hears which level each swatch is. */
.map-key.is-folded .map-legend-label {
  position: absolute;
  width: 1px; height: 1px;
  clip-path: inset(50%);
  overflow: hidden;
  white-space: nowrap;
}

.map-legend-item {
  display: flex;
  align-items: center;
  gap: 9px;
  width: 100%;
  text-align: left;
  font: inherit;
  font-size: 13.5px;
  line-height: 1.5;
  padding: 5px 7px;
  cursor: pointer;
  color: var(--ink);
  background: none;
  border: 1px solid transparent;
  border-radius: 3px;
}
.map-legend-item:hover { background: rgba(255,255,255,.35); }

/* A real checkbox, kept at a size you can actually hit. Its own tick is
   the primary signal that a level is on — the strike-through below is
   the second one, because the site's rule is that no state is carried by
   one cue alone. */
.map-legend-check {
  flex: 0 0 auto;
  width: 15px; height: 15px;
  margin: 0;
  accent-color: #6b4a17;
  cursor: pointer;
}
/* Unchecked: struck through as well as faded, so the word says it too.
   Driven by a class the script sets, not by :has() — the strike-through
   is a state cue, and a state cue should not rest on a selector the
   stylesheet cannot guarantee. */
.map-legend-item.is-off { opacity: .6; }
.map-legend-item.is-off .map-legend-label { text-decoration: line-through; }
/* Focus has to be visible on the label, since the box itself is small
   and sits on a busy parchment. :focus-within is old and safe. */
.map-legend-item:focus-within {
  border-color: var(--gold);
  background: rgba(255,255,255,.35);
}

.map-legend-swatch {
  flex: 0 0 auto;
  width: 15px; height: 15px;
  border-radius: 50%;
  border: 1px solid rgba(20,14,8,.45);
}
.map-panel-danger .map-legend-swatch { margin-top: 3px; }

/* ---- the list under the map ---- */

.map-index { margin-top: 38px; }

/* The filter bar. One level at a time, plus All — see drawIndex(). */
.map-filter {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin: 0 0 18px;
}
.map-filter-item {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  font: inherit;
  font-size: 13.5px;
  line-height: 1.4;
  padding: 7px 13px;
  cursor: pointer;
  color: var(--ink);
  background: rgba(255,255,255,.16);
  border: 1px solid rgba(20,14,8,.28);
  border-radius: 999px;
}
.map-filter-item:hover { background: rgba(255,255,255,.38); }
/* The chosen one is stated, not just tinted: darker ground, a real
   border and bold text, so it survives being read without colour. */
.map-filter-item[aria-pressed="true"] {
  font-weight: bold;
  background: var(--parchment-light);
  border-color: var(--gold);
  box-shadow: inset 0 0 0 1px var(--gold);
}

/* Flat and alphabetical: no groups any more. The swatch on each row is
   what replaces the group headings — the level is still legible without
   filtering for it. */
.map-index-list {
  margin: 0;
  padding-left: 0;
  list-style: none;
  columns: 3;
  column-gap: 40px;
}
.map-index-list li {
  display: flex;
  align-items: baseline;
  gap: 8px;
  line-height: 1.7;
  margin-bottom: 5px;
  break-inside: avoid;
}
.map-index-list li .map-legend-swatch {
  width: 11px; height: 11px;
  align-self: center;
}
.map-index-descend { font-size: 12.5px; color: var(--muted); }
.map-index-empty { margin: 4px 0 0; }

@media (max-width: 900px) { .map-index-list { columns: 2; } }

/* Below 700px the key stops being an overlay. A card floating over a map
   that is itself only a few hundred pixels wide covers the thing it is
   meant to explain, and there is no corner far enough from your thumb to
   put it in. It becomes an ordinary block under the frame instead —
   same markup, same buttons, and unfolded, because down here it is no
   longer in anybody's way. */
@media (max-width: 700px) {
  .map-key {
    position: static;
    max-width: none;
    margin-top: 14px;
    box-shadow: none;
  }
  .map-key.is-folded .map-key-title { display: block; }
  .map-key.is-folded .map-key-list { display: block; margin: 8px 0 0; }
  .map-key.is-folded .map-key-head { flex-direction: row; }
  .map-key.is-folded .map-legend-item { padding: 5px 7px; }
  .map-key.is-folded .map-legend-label {
    position: static;
    width: auto; height: auto;
    clip-path: none;
  }
  .map-key-fold { display: none; }
}

@media (max-width: 600px) {
  .map-index-list { columns: 1; }
  .map-stage { height: 58vh; }
  /* Docked to the bottom on a phone: floating over a map you are pinching
     puts the panel under your own hand. */
  .map-panel {
    position: static;
    width: auto;
    margin-top: 14px;
  }
}
