/* Cinzel display face, self-hosted (2026-08-18) -- previously pulled from
   the Google Fonts CDN, the one external network dependency in an
   otherwise fully offline, no-build game; offline it silently degraded to
   the Georgia fallback below. Two files (not the usual single static
   weight file) because these ARE Google's real served files for a
   combined 400/700/900 request -- Google multiplexes one variable-weight
   font across all three nominal @font-face blocks rather than shipping 3
   separate static instances, and modern browsers resolve the requested
   font-weight against the file's own wght axis. `font-weight: 400 900` is
   a RANGE, matching that -- collapsing it to a single fixed weight would
   silently flatten every bold/black heading back to regular. latin-ext
   loads first per CSS's normal fallback order (later rule = higher
   priority for the same unicode-range overlap is irrelevant here; both
   are needed since they cover disjoint code points, see each
   unicode-range). Licensed OFL-1.1 -- see assets/fonts/OFL.txt, credited
   in credits.txt. */
@font-face {
  font-family: "Cinzel";
  font-style: normal;
  font-weight: 400 900;
  font-display: swap;
  src: url("../assets/fonts/cinzel-latin-ext-variable.woff2") format("woff2");
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: "Cinzel";
  font-style: normal;
  font-weight: 400 900;
  font-display: swap;
  src: url("../assets/fonts/cinzel-latin-variable.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

:root {
  --sidebar-width: 280px;
  --bg-dark: #14171c;
  --bg-panel: #1d2128;
  --bg-panel-raised: #262b34;
  --border: #383e4a;
  --text-main: #e4e6ea;
  --text-dim: #8b92a0;
  --accent: #c9a857;
  --accent-hover: #e0bc6a;
}

* { box-sizing: border-box; }

html, body {
  margin: 0; padding: 0;
  width: 100%; height: 100%;
  background: var(--bg-dark);
  color: var(--text-main);
  font-family: "Georgia", "Iowan Old Style", serif;
  overflow: hidden;
}

/* --- Title screen --- */
.title-screen {
  width: 100%; height: 100vh;
  display: flex; flex-direction: column;
}


/* Main area: background image + centered title text */
.title-main {
  flex: 1;
  position: relative;
  overflow: hidden;
  display: flex;
  align-items: center;
  justify-content: center;
}

/*
 * Background image layer. Place your title artwork at img/title-bg.jpg.
 * The image is faded via opacity so the title text reads clearly over it.
 * Without the image file the gradient fallback provides a suitable backdrop.
 */
.title-bg-overlay {
  position: absolute;
  inset: 0;
  background-color: #0e1014;
  background-image: url('../assets/img/title-bg.png');
  background-size: cover;
  background-position: center;
  opacity: 0.30;
}

/* Title logo + text: rendered above the image layer */
.title-content {
  position: relative;
  z-index: 1;
  text-align: center;
  user-select: none;
}
/*
 * Title logo (assets/img/logo.png, 701x629). Sized to ~50% of the viewport
 * height with width auto to preserve its aspect ratio; centered via the
 * parent's text-align + auto side margins. (Was `100vh - 52px` back when a
 * launch toolbar took a fixed strip off the top -- that toolbar is gone, its
 * controls now live in the Game Options modal.)
 */
.title-logo {
  display: block;
  height: 50vh;
  width: auto;
  margin: 0 auto;
}
/* The splash screen's main button -- opens the launch options modal, with
   the Mute Audio checkbox tucked underneath it (see .title-mute-row). */
.title-options-btn {
  margin-top: 36px;
  padding: 11px 34px;
  background: rgba(201, 168, 87, 0.12);
  border: 1px solid rgba(201, 168, 87, 0.55);
  border-radius: 4px;
  color: var(--accent);
  font-family: "Cinzel", Georgia, serif;
  font-size: 15px;
  letter-spacing: 0.1em;
  cursor: pointer;
  transition: background 0.2s, border-color 0.2s, color 0.2s;
}
.title-options-btn:hover {
  background: var(--accent);
  border-color: var(--accent);
  color: #1a1a1a;
}
/* "Mute Audio", directly under Begin (2026-08-26, user-directed -- replaces
   the Game Options modal's old Audio section). Deliberately quiet: this sits
   on the splash art under the one button that matters, so it reads as a
   footnote to Begin rather than a second call to action. text-shadow because
   it sits on a photographic background, same reason title-build-stamp has
   one. */
.title-mute-row {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  margin-top: 14px;
  color: var(--text-dim);
  font-size: 12px;
  letter-spacing: 0.04em;
  cursor: pointer;
  text-shadow: 0 1px 3px rgba(0, 0, 0, 0.8);
}
.title-mute-row:hover { color: var(--accent); }
.title-mute-row input { cursor: pointer; margin: 0; }
/* --- Loading screen: between clicking Begin and the game being ready --- */
.loading-screen {
  position: fixed;
  inset: 0;
  z-index: 50;
  display: flex;
  align-items: center;
  justify-content: center;
  background: #0e1014;
}
.loading-bg-overlay {
  position: absolute;
  inset: 0;
  background-image: url('../assets/img/title-bg.png');
  background-size: cover;
  background-position: center;
  opacity: 0.30;
}
.loading-content {
  position: relative;
  z-index: 1;
  text-align: center;
  user-select: none;
}
.loading-logo {
  display: block;
  height: 22vh;
  width: auto;
  margin: 0 auto 28px;
  opacity: 0.9;
}
.loading-spinner {
  width: 34px;
  height: 34px;
  margin: 0 auto;
  border: 3px solid rgba(201, 168, 87, 0.25);
  border-top-color: rgba(201, 168, 87, 0.9);
  border-radius: 50%;
  animation: loading-spin 0.9s linear infinite;
}
@keyframes loading-spin {
  to { transform: rotate(360deg); }
}
/* Reduced motion (js/ui/motion.js stamps this attribute on <html>,
   resolving the in-game Auto/Full/Reduced control against the OS
   prefers-reduced-motion query -- see that module's doc comment for why
   this is NOT a plain @media query): freeze the spin, keep the ring
   itself visible as a static "loading" indicator rather than removing it
   -- it's still communicating real state, just not by rotating. */
:root[data-motion-reduced="true"] .loading-spinner {
  animation: none;
  border-top-color: rgba(201, 168, 87, 0.9);
}
.loading-status {
  margin: 20px 0 0 0;
  font-family: "Cinzel", Georgia, serif;
  font-size: clamp(12px, 1.5vw, 16px);
  color: var(--text-dim);
  letter-spacing: 0.1em;
  text-shadow: 0 1px 4px rgba(0, 0, 0, 0.8);
}
.loading-progress-list {
  margin: 18px auto 0;
  width: min(320px, 60vw);
  display: flex;
  flex-direction: column;
  gap: 8px;
}
.loading-progress-row {
  display: flex;
  align-items: center;
  gap: 10px;
}
.loading-progress-label {
  flex: 0 0 88px;
  text-align: right;
  font-family: "Cinzel", Georgia, serif;
  font-size: 11px;
  color: var(--text-dim);
  letter-spacing: 0.03em;
}
.loading-progress-track {
  flex: 1 1 auto;
  height: 6px;
  border-radius: 3px;
  background: rgba(255, 255, 255, 0.08);
  border: 1px solid rgba(201, 168, 87, 0.2);
  overflow: hidden;
}
.loading-progress-fill {
  height: 100%;
  width: 0%;
  background: var(--accent);
  transition: width 0.15s ease-out;
}
.loading-progress-pct {
  flex: 0 0 32px;
  font-size: 11px;
  color: var(--text-dim);
  font-variant-numeric: tabular-nums;
}

/* --- Game screen --- */
.game-screen {
  display: flex;
  flex-direction: column;
  width: 100%;
  /* 100vh on a mobile browser means the viewport WITH the address bar and
     toolbar counted in, even while they're on screen -- so the bottom of the
     UI sits underneath them until the user scrolls. 100dvh tracks the chrome
     as it slides in and out. vh first as the fallback for anything that
     doesn't know dvh; the dvh line simply wins where it's supported. */
  height: 100vh;
  height: 100dvh;
}
/* Body row (map + sidebar), below the top menu bar -- same two-region split
   as before, just no longer the full-height flex row itself. min-height: 0
   lets the map/sidebar's own overflow rules apply within the remaining
   space instead of the flex column stretching to fit their content. */
.game-body {
  flex: 1;
  display: flex;
  min-height: 0;
  /* Containing block for .daynight-clock-docked, which hangs over the
     sidebar's top border from out here rather than from inside the
     sidebar's own (clipping) scroll box. */
  position: relative;
}
.map-area {
  flex: 1;
  position: relative;
  background: #0a0c0f;
}
#map-canvas {
  width: 100%;
  height: 100%;
  display: block;
  cursor: grab;
  /* Without this the browser claims touch gestures before input.js ever sees
     them: a one-finger drag becomes page scroll and a pinch becomes page
     zoom, so panning and zooming the MAP would be impossible on a phone.
     Pointer events still arrive; we just get to decide what they mean. */
  touch-action: none;
}
/* Drifting cloud layer (2026-08-06, user-directed) -- purely cosmetic
   atmosphere floating above the map. pointer-events:none so it can never
   be a hit target: every click, drag, wheel and hover falls straight
   through to #map-canvas underneath, exactly as if this layer weren't
   here. Same treatment #map-canvas-3d-hud gets below, and for the same
   reason. See js/ui/clouds.js. */
/* Rain and storm overlay (js/ui/weather.js). Sits above the clouds: rain is
   between the viewer and everything else, sky included. Same non-interactive
   treatment -- a click over the map must fall straight through. */
#map-weather,
#map-clouds {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
}
/* ASTRONOMICAL CLOCK (js/ui/daynight-clock.js) -- the lower half of a circle
   showing where the 12-turn day/night cycle currently is.

   Two homes (2026-09-09, user-directed "move the clock to the top of the
   toolbar"). On desktop it sits in the sidebar footer's own
   #daynight-clock-slot, above End Turn, as a plain block in that column's
   flow -- so the rest of the toolbar simply falls beneath it. On mobile
   that footer lives inside the collapsible bottom sheet, which would hide
   the clock whenever the sheet is shut, so there it keeps floating over
   .map-area instead and picks up .daynight-clock-floating below.

   pointer-events:none in both, for the same reason #map-clouds has it: this
   reports state, it is never a hit target, and a click over the map must
   fall straight through to the canvas underneath. (Hover for the phase name
   is done by script against cursor coordinates -- see the module's own doc
   comment -- precisely so this can stay non-interactive.) */
.daynight-clock {
  pointer-events: none;
  /* The canvas paints its own rim; this is the drop shadow that lifts the
     whole dial off whatever it sits on. Clipped to the half-disc so no
     square edge shows. */
  filter: drop-shadow(0 2px 6px rgba(0, 0, 0, 0.45));
}
/* DESKTOP: docked over the sidebar's own top edge, deliberately overlapping
   the gilded border-image so the dial reads as mounted on the frame rather
   than floating inside the panel. The rest of the sidebar's content is
   pushed clear of it by .sidebar's padding-top below.

   Parented to .game-body, NOT to .sidebar, and that is load-bearing: the
   sidebar is overflow-y:auto, which clips at its padding box, so a child of
   it could never draw onto the 16px border at all -- it would just be cut
   off at the inner edge. Anchoring to the sidebar's own centre line from
   outside sidesteps that entirely.

   The right/transform pair centres the dial on the sidebar column without
   this rule needing to know how wide the dial is: put its right edge on the
   column's centre line, then shove it back by half its own width. */
.daynight-clock-docked {
  position: absolute;
  top: 0;
  right: calc(var(--sidebar-width) / 2);
  transform: translateX(50%);
  /* Over the sidebar's border art, still under the ring menu (500) and
     every modal -- it must never sit over a dialog the player is reading. */
  z-index: 7;
}
/* MOBILE (and any other over-the-map placement): a free-floating overlay.
   See css/mobile.css for where it actually lands.

   `fixed`, NOT `absolute` (2026-09-12, user-reported: "the top of the clock
   should be aligned with, and overlap, the top of the slide up toolbar...
   right now, it sits mostly above" -- on a real phone with a home
   indicator, not in a desktop-emulated mobile viewport, which is why this
   didn't show up in the obvious way of testing it). `absolute` made this
   position relative to .map-area, whose OWN bottom edge is NOT the true
   screen edge on mobile: body.mobile .game-screen carries
   `padding-bottom: env(safe-area-inset-bottom)` (mobile.css), and .map-area
   (`inset: 0`) fills that already-inset padding box, so its bottom sits
   ABOVE the real viewport bottom by however tall the device's home
   indicator inset is (~34px on an iPhone with one). #sidebar, the sheet
   this dial has to track, is `position: fixed` and deliberately ignores
   that exact same inset to sit flush with the true screen edge (see
   mobile.css's own comment on that choice) -- so the dial was drifting
   away from the sheet by exactly the inset's height on any device that
   actually has one. `fixed` anchors this to the true viewport instead,
   the same reference frame #sidebar already uses, closing the gap on any
   device regardless of its own safe-area inset. */
.daynight-clock-floating {
  position: fixed;
  top: 0;
  left: 50%;
  transform: translateX(-50%);
  z-index: 6;
}
.daynight-clock canvas {
  display: block;
}

#map-canvas-3d {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  display: none;
  cursor: grab;
}
/* Screen-space HUD overlay (HP bars, selection rings, condition badges,
   quips, floating text, combat slashes) for the 3D view -- layered exactly
   over #map-canvas-3d so projected screen coordinates line up pixel-for-
   pixel. pointer-events:none so clicks fall through to the 3D canvas's own
   click-to-select handler underneath. */
#map-canvas-3d-hud {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  display: none;
  pointer-events: none;
}
/* Top menu bar: a full-width strip above the map (like the title screen's
   toolbar), not an overlay floating on top of it. */
.menu-bar {
  display: flex;
  align-items: flex-start;
  flex-wrap: wrap;
  gap: 2px;
  background: rgba(20, 23, 28, 0.96);
  border-bottom: 1px solid var(--border);
  padding: 4px 8px;
  flex-shrink: 0;
}
.menu-item { position: relative; }
.menu-btn {
  background: transparent;
  border: 1px solid transparent;
  color: var(--text-main);
  padding: 6px 12px;
  border-radius: 3px;
  cursor: pointer;
  font-family: inherit;
  font-size: 12px;
}
.menu-btn:hover { border-color: var(--accent); }
.menu-btn.active { background: var(--bg-panel-raised); border-color: var(--accent); color: var(--accent); }

.menu-dropdown {
  position: absolute;
  top: calc(100% + 4px); left: 0;
  background: rgba(29, 33, 40, 0.97);
  border: 1px solid var(--border);
  border-radius: 4px;
  padding: 8px;
  display: flex;
  flex-direction: column;
  gap: 6px;
  min-width: 190px;
  z-index: 10;
}
.menu-dropdown-btn {
  background: var(--bg-panel-raised);
  border: 1px solid var(--border);
  color: var(--text-main);
  padding: 6px 10px;
  border-radius: 3px;
  cursor: pointer;
  font-size: 12px;
  font-family: inherit;
  text-align: left;
}
.menu-dropdown-btn:hover { border-color: var(--accent); }
/* Menu item with a keyboard-shortcut hint pushed to the right edge. */
.menu-dropdown-btn-shortcut {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 14px;
}
.menu-shortcut {
  color: var(--text-dim);
  font-size: 11px;
  letter-spacing: 0.04em;
}

.audio-panel {
  min-width: 320px;
}
.audio-panel-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  font-size: 12px;
  color: var(--text-main);
}
.audio-panel-row input[type="checkbox"] { accent-color: var(--accent); margin: 0; }
.audio-panel-row input[type="range"] { accent-color: var(--accent); width: 100px; }
.audio-panel-row select {
  background: var(--bg-panel-raised);
  border: 1px solid var(--border);
  color: var(--text-main);
  border-radius: 3px;
  padding: 3px 6px;
  font-family: inherit;
  font-size: 12px;
  max-width: 130px;
}
.now-playing-row { border-bottom: 1px solid var(--border); padding-bottom: 6px; margin-bottom: 2px; }
.now-playing-row span:first-child { color: var(--text-dim); }
.now-playing-value {
  font-weight: bold;
  max-width: 260px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  text-align: right;
}

.speed-btn-row { display: flex; gap: 4px; }
.speed-btn {
  flex: 1;
  background: var(--bg-panel-raised);
  border: 1px solid var(--border);
  color: var(--text-main);
  padding: 4px 10px;
  border-radius: 3px;
  cursor: pointer;
  font-size: 12px;
  font-family: inherit;
}
.speed-btn.active { background: var(--accent); color: #1a1a1a; border-color: var(--accent); }

/* Map zoom control -- pinned to the far right of the top menu bar via
   margin-left: auto, independent of however many menu items sit to its left. */
/* Desktop turn/resource + research-progress status (2026-08-27, user-
   directed: bring the mobile status/research pills to desktop too) -- sits
   in the menu bar's own row, pushed toward the zoom control on the right
   via this one's own margin-left:auto (the research pill, having no
   content when hidden, doesn't need one of its own -- it just follows
   directly after). */
.d-status {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-left: auto;
  padding: 4px 10px;
  border: 1px solid var(--border);
  border-radius: 4px;
  background: var(--bg-panel-raised);
  font-size: 12px;
  font-variant-numeric: tabular-nums;
  color: var(--text-main);
}
#d-status-turn { color: var(--text-dim); }
#d-status-res { display: flex; align-items: center; gap: 8px; font-weight: 600; }
.d-research {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 4px 10px;
  border: 1px solid var(--border);
  border-radius: 4px;
  background: var(--bg-panel-raised);
  font-size: 12px;
}
.d-research[hidden] { display: none; }
#d-research-label {
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  max-width: 110px;
}
.d-research-track {
  width: 60px;
  height: 5px;
  border-radius: 3px;
  background: rgba(255, 255, 255, .14);
  overflow: hidden;
  flex: none;
}
/* Width transitions once per turn at most (research only ever progresses),
   so this is a gentle one-directional fill, not the flashing/strobing the
   project's own no-flashing rule is about -- same reasoning as the mobile
   pill's identical rule in css/mobile.css. */
.d-research-fill {
  height: 100%;
  width: 0%;
  background: var(--accent);
  border-radius: 3px;
  transition: width .3s ease;
}

.zoom-control {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 0 4px;
}
.zoom-btn {
  width: 26px;
  height: 26px;
  background: transparent;
  border: 1px solid var(--border);
  color: var(--text-main);
  border-radius: 3px;
  cursor: pointer;
  font-family: inherit;
  font-size: 16px;
  line-height: 1;
  display: flex;
  align-items: center;
  justify-content: center;
}
.zoom-btn:hover { border-color: var(--accent); color: var(--accent); }
.zoom-level-label {
  font-size: 12px;
  color: var(--text-dim);
  min-width: 36px;
  text-align: center;
  user-select: none;
}

.fow-panel {
  display: flex;
  flex-direction: column;
  gap: 6px;
  padding-top: 6px;
  margin-top: 2px;
  border-top: 1px solid var(--border);
}
.fow-panel-label {
  font-size: 11px;
  font-weight: 700;
  color: var(--text-dim, var(--text-main));
  text-transform: uppercase;
  letter-spacing: 0.03em;
}
.fow-panel .audio-panel-row input[type="radio"] { accent-color: var(--accent); margin: 0; }
.fow-race-list {
  display: flex;
  flex-direction: column;
  gap: 4px;
  padding-left: 10px;
  max-height: 140px;
  overflow-y: auto;
}

/* --- Sidebar --- */
.sidebar {
  /* Shared with .daynight-clock-docked, which centres itself over this
     column from OUTSIDE it -- see that rule. One variable so the two can't
     drift apart if the sidebar is ever resized. */
  width: var(--sidebar-width);
  background: var(--bg-panel);
  /* Gilded frame (2026-08-18, user-directed): one border-image per kingdom
     (see assets/img/sidebar-gilding-<race>.svg), swapped via a `race-*`
     modifier class that sidebar.js's render() sets on this element every
     redraw from gameState.civs[humanCivId].raceId (or "spectator" when
     there's no human civ). The default source below is only what shows
     before the first render() call ever runs. Border on all four sides --
     each SVG already draws right-edge/right-corner art (mirrored, not
     just left/top/bottom), so a right:0 border was hiding art that was
     always there rather than actually saving space. */
  border-width: 16px;
  border-style: solid;
  border-image-slice: 20;
  border-image-repeat: round;
  border-image-source: url("../assets/img/sidebar-gilding-human.svg");
  display: flex;
  flex-direction: column;
  overflow-y: auto;
  padding: 14px;
  /* Headroom for the astronomical clock docked over this panel's top edge
     (.daynight-clock-docked). The dial is half of a 135px circle, so ~68px
     tall, of which the top 16px sits on the border itself -- leaving ~52px
     reaching into the panel, plus a little breathing room before the first
     control. Overridden back to normal on mobile, where the clock isn't
     docked here at all (see mobile.css). */
  padding-top: 62px;
}
.sidebar.race-human, .game-dialog-victory.race-human, .game-dialog-story.race-human, .story-bark.race-human         { border-image-source: url("../assets/img/sidebar-gilding-human.svg"); }
.sidebar.race-elf, .game-dialog-victory.race-elf, .game-dialog-story.race-elf, .story-bark.race-elf             { border-image-source: url("../assets/img/sidebar-gilding-elf.svg"); }
.sidebar.race-dwarf, .game-dialog-victory.race-dwarf, .game-dialog-story.race-dwarf, .story-bark.race-dwarf         { border-image-source: url("../assets/img/sidebar-gilding-dwarf.svg"); }
.sidebar.race-orc, .game-dialog-victory.race-orc, .game-dialog-story.race-orc, .story-bark.race-orc             { border-image-source: url("../assets/img/sidebar-gilding-orc.svg"); }
.sidebar.race-undead, .game-dialog-victory.race-undead       { border-image-source: url("../assets/img/sidebar-gilding-undead.svg"); }
.sidebar.race-halfellow, .game-dialog-victory.race-halfellow, .game-dialog-story.race-halfellow, .story-bark.race-halfellow { border-image-source: url("../assets/img/sidebar-gilding-halfellow.svg"); }
.sidebar.race-spectator { border-image-source: url("../assets/img/sidebar-gilding-spectator.svg"); }
.panel h2 {
  margin: 0 0 12px 0;
  font-size: 20px;
  color: var(--accent);
  border-bottom: 1px solid var(--border);
  padding-bottom: 8px;
}
.panel h3 {
  margin: 16px 0 6px 0;
  font-size: 13px;
  color: var(--text-dim);
  text-transform: uppercase;
  letter-spacing: 0.5px;
}
.stat-row {
  display: flex;
  justify-content: space-between;
  padding: 3px 0;
  font-size: 13px;
}
.stat-row span:first-child { color: var(--text-dim); }
.stat-row span:last-child { color: var(--text-main); font-weight: bold; }
/* Kingdom panel's per-resource economy grid (2026-08-18) -- replaces the
   old "Income (H / C / L)" triple-value rows, which forced the player to
   hold a legend in their head and parse three numbers out of one string.
   One row per resource with its own icon instead. economy-grid-stock adds
   a 3rd value column when a stockpile figure is known; the plain
   .economy-grid stays 2 value columns (Income/Upkeep) otherwise. Column
   count is small and fixed either way, so this is shorter top-to-bottom
   than the 4 rows it replaces despite showing the same data plus icons --
   see input.js's own history of sidebar content pushing End Turn off the
   bottom of a 280px-wide panel. Column count here must track the number
   of header/row cells sidebar.js actually emits -- a mismatch leaves the
   header row short a cell, which shifts every row after it over by one
   column instead of wrapping to a new line. */
.economy-grid {
  display: grid;
  grid-template-columns: 1fr repeat(2, minmax(30px, 1fr));
  gap: 3px 4px;
  align-items: center;
}
.economy-grid.economy-grid-stock {
  grid-template-columns: 1fr repeat(3, minmax(26px, 1fr));
}
.economy-grid-header {
  color: var(--text-dim);
  font-size: 10px;
  text-transform: uppercase;
  letter-spacing: 0.3px;
  text-align: right;
  padding-bottom: 3px;
  border-bottom: 1px solid var(--border);
}
.economy-grid-header:first-child {
  text-align: left;
}
.economy-res {
  display: flex;
  align-items: center;
  gap: 5px;
  color: var(--text-main);
  font-size: 13px;
}
.economy-res .resource-icon {
  width: 13px;
  height: 13px;
  flex: none;
  color: var(--accent);
}
/* Base sizing for every #icon-harvest/#icon-coin/#icon-lore glyph used
   inline next to a number outside the economy table itself -- cost tokens
   (buildlist.js/techtree.js) and stockpile readouts (2026-08-27, swapped in
   for the old bare "H"/"C"/"L" letter suffix). A more specific selector
   (like .economy-res above, or .m-status-res in mobile.css) still wins where
   one already exists; this only fills in the default everywhere else so
   nothing renders at the SVG's unstyled native size. */
.resource-icon {
  width: 12px;
  height: 12px;
  vertical-align: -2px;
  flex: none;
}
.economy-val {
  text-align: right;
  font-weight: bold;
  font-size: 13px;
  color: var(--text-main);
  font-variant-numeric: tabular-nums;
}
.economy-val.economy-negative { color: #f0a830; }
/* Idle-city tag (2026-08-07, user-directed): an FYI, not an error -- amber
   like the rest of the accent palette, not the danger red action buttons
   use for destructive actions. */
.idle-tag {
  display: inline-block;
  padding: 1px 6px;
  margin-right: 6px;
  border: 1px solid rgba(201, 168, 87, 0.55);
  border-radius: 3px;
  background: rgba(201, 168, 87, 0.12);
  color: var(--accent);
  font-size: 10px;
  font-weight: bold;
  letter-spacing: 0.04em;
  text-transform: uppercase;
}

/* Pinned to the bottom of the sidebar (2026-08-06, user-directed): End Turn
   used to scroll off-screen exactly when the panel was busiest. `margin-top:
   auto` alone only pushes the footer down while the content ABOVE it is
   shorter than the sidebar -- the moment a panel overflows (a garrisoned
   capital, a Wizard with pending level-ups) that collapses and the footer
   scrolls away with everything else. Sticky handles the tall case, the auto
   margin still handles the short case; both are needed.

   bottom is -16px, not 0, to cancel .sidebar's own 16px bottom padding --
   sticky measures against the padding box, so `bottom: 0` would pin the
   footer 16px up and leave a strip of scrolled content visible beneath it.
   The footer's own padding-bottom fills that 16px back in. The opaque
   background is load-bearing too (without it the rows scrolling underneath
   show straight through), as is z-index (stat-rows would otherwise paint
   over it). */
.sidebar-footer {
  position: sticky;
  bottom: -16px;
  z-index: 1;
  margin-top: auto;
  padding: 16px 0;
  background: var(--bg-panel);
  border-top: 1px solid var(--border);
}
.turn-counter {
  text-align: center;
  color: var(--text-dim);
  font-size: 12px;
  margin-bottom: 8px;
}
/* Territorial-victory progress, in the always-visible sidebar footer -- see
   sidebar.js's territoryHtml. Sits directly above End Turn so the win
   condition is legible every turn instead of only from the Kingdom panel or
   the Reports overlay. The fill measures progress toward the THRESHOLD, not
   toward 100% of the map. */
.territory-progress {
  margin-bottom: 8px;
}
.territory-progress-label {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 8px;
  font-size: 12px;
  color: var(--text-dim);
  margin-bottom: 4px;
}
/* Only rendered when another civ is actually ahead, so it reads as a warning
   rather than a permanent fixture. */
.territory-leader {
  color: var(--text-dim);
  opacity: 0.75;
  white-space: nowrap;
}
.territory-progress-track {
  height: 6px;
  border-radius: 3px;
  background: var(--bg-dark, rgba(0, 0, 0, 0.35));
  border: 1px solid var(--border);
  overflow: hidden;
}
.territory-progress-fill {
  height: 100%;
  background: var(--accent);
  transition: width 200ms ease-out;
}
/* "Automated" banner row in the city panel's Building section -- see
   sidebar.js's renderBuildSection. Accent-tinted so a delegated city is
   distinguishable at a glance from one the player is still driving. */
.city-automated-row {
  color: var(--accent);
}
.end-turn-btn {
  width: 100%;
  padding: 12px;
  background: var(--accent);
  border: none;
  border-radius: 4px;
  color: #1a1a1a;
  font-size: 15px;
  font-weight: bold;
  cursor: pointer;
  font-family: inherit;
}
.end-turn-btn:hover { background: var(--accent-hover); }
/* Nothing left to order this turn (2026-08-26, user-directed): the button
   itself says so with color, same "Colour, not motion" no-flashing rule
   the mobile FAB's own .m-ready follows -- see css/mobile.css. Same green
   as that FAB's ready state (rgba(58,122,74)) for the two to read as one
   idea in two places, solid rather than translucent since this button
   never sits over game art the way the floating FAB does. */
.end-turn-btn-ready { background: #3a7a4a; color: #fff; }
.end-turn-btn-ready:hover { background: #468f59; }

/* --- Tabbed tile inspector ---
   One tab per thing on the selected tile (city / each unit / building /
   terrain / kingdom); see js/ui/input.js's SELECTION MODEL. The strip wraps
   rather than scrolls -- a tile can hold several tabs and the sidebar is only
   280px, so a hidden overflow would silently swallow them. */
/* File-folder tab look (2026-08-07, user-directed) -- was a row of equal-
   weight rounded buttons with a gap under the whole strip; now each tab has
   only its top corners rounded and sits directly on a shared fold line
   (.tile-tabs' own border-bottom). The active tab stands 2px proud of the
   inactive ones (top: 0 vs top: 2px) and its own bottom border is painted
   the PANEL's background color, which erases exactly the segment of the
   fold line under it -- the classic "this tab is physically part of the
   page below it; the others are tucked behind" illusion, rather than a
   filled button that merely indicates selection by color. */
.tile-tabs {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: 2px;
  margin-bottom: 12px;
  border-bottom: 2px solid var(--border);
}
.tile-tab {
  position: relative;
  top: 2px;
  background: var(--bg-panel-raised);
  border: 1px solid var(--border);
  border-bottom: 2px solid var(--border);
  border-radius: 5px 5px 0 0;
  color: var(--text-dim);
  font-family: inherit;
  font-size: 11px;
  padding: 4px 9px 3px;
  cursor: pointer;
  white-space: nowrap;
}
.tile-tab:hover { color: var(--text-main); border-color: var(--accent); }
.tile-tab-active {
  top: 0;
  z-index: 1;
  background: var(--bg-panel);
  border-color: var(--border);
  border-bottom-color: var(--bg-panel);
  padding-top: 5px;
  color: var(--accent);
  font-weight: bold;
}
.tile-tab-active:hover { color: var(--accent-hover); }

/* Shortcut rows that jump to another tab (city panel's garrison list,
   terrain panel's contents list). Deliberately shaped like a .stat-row so
   the panels stay visually uniform -- it just happens to be clickable. */
/* Inline jump-to-tile link (see sidebar.js's tileLink/linkifyCoords). Sits
   INSIDE a line of text, unlike .tile-tab-link below, which is a full-width
   row -- so it's styled as a link rather than as a button. */
.tile-link, .dialog-action-link {
  display: inline;
  padding: 0;
  background: none;
  border: none;
  color: var(--accent);
  font-family: inherit;
  font-size: inherit;
  text-align: left;
  text-decoration: underline;
  text-decoration-style: dotted;
  text-underline-offset: 2px;
  cursor: pointer;
}
.tile-link:hover, .dialog-action-link:hover { color: var(--accent-hover); text-decoration-style: solid; }

.tile-tab-link {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 8px;
  width: 100%;
  padding: 3px 4px;
  margin: 1px 0 1px -4px;
  background: none;
  border: 1px solid transparent;
  border-radius: 3px;
  font-family: inherit;
  font-size: 13px;
  text-align: left;
  cursor: pointer;
}
.tile-tab-link span:first-child { color: var(--text-dim); }
.tile-tab-link span:last-child { color: var(--text-main); font-weight: bold; }
.tile-tab-link:hover {
  border-color: var(--accent);
  background: var(--bg-panel-raised);
}
.tile-tab-link:hover span:first-child { color: var(--accent); }

/* --- City production picker --- */
.build-group-label {
  margin: 10px 0 4px 0;
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.5px;
  color: var(--text-dim);
}
.build-option {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: 2px;
  width: 100%;
  padding: 5px 6px;
  margin: 2px 0;
  background: var(--bg-panel-raised);
  border: 1px solid var(--border);
  border-radius: 3px;
  font-family: inherit;
  font-size: 12px;
  text-align: left;
  cursor: pointer;
}
/* The top label+price line -- was the whole button's own flex row before
   the per-unit stat line (2026-08-11) needed a second row underneath it. */
.build-option-row {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 8px;
  width: 100%;
}
.build-option span:first-child { color: var(--text-main); }
.build-option span:last-child { color: var(--text-dim); font-family: monospace; white-space: nowrap; }
.build-option:hover:not(:disabled) { border-color: var(--accent); }
.build-option:hover:not(:disabled) span:first-child { color: var(--accent); }
.build-option-unaffordable { opacity: 0.4; cursor: not-allowed; }
.build-option-stats {
  color: var(--accent);
  font-size: 11px;
  font-weight: 600;
  white-space: normal;
}

.build-progress {
  height: 5px;
  background: var(--bg-panel-raised);
  border-radius: 3px;
  overflow: hidden;
  margin: 4px 0 8px 0;
}
.build-progress-fill { height: 100%; background: var(--accent); }

/* HP-style meter (2026-08-04, user-reported): structures and city population,
   both places where a chunk of remaining "how much punishment is left"
   matters at a glance and previously only had a bare number. Same visual
   shape as .build-progress above (thin bar, rounded, panel-raised track) but
   its own class since the fill color is dynamic (green/yellow/red by
   remaining fraction) rather than always the accent color. */
.hp-bar {
  height: 5px;
  background: var(--bg-panel-raised);
  border-radius: 3px;
  overflow: hidden;
  margin: 3px 0 8px 0;
}
.hp-bar-fill { height: 100%; }

/* Structure placement is a modal cursor mode -- the banner has to be
   impossible to miss, since left-click means something different while it's up. */
.placement-banner {
  padding: 10px;
  margin-bottom: 12px;
  background: rgba(255, 215, 90, 0.12);
  border: 1px solid #ffd75a;
  border-radius: 4px;
  color: #ffd75a;
  font-size: 12px;
  line-height: 1.45;
}

/* Peek panel banner (2026-09-02) -- see input.js's INSPECT doc comment.
   Deliberately quieter than .placement-banner above: nothing is pending or
   needs a decision here, this just flags that the panel below is a look at
   some OTHER tile, not the player's actual selection. */
.sidebar-inspect-banner {
  padding: 8px 10px;
  margin-bottom: 12px;
  background: rgba(120, 170, 220, 0.10);
  border: 1px solid rgba(120, 170, 220, 0.5);
  border-radius: 4px;
  color: #9fc2e0;
  font-size: 11px;
  line-height: 1.4;
}

/* --- Awaiting-orders cycler (sidebar footer) --- */
.next-unit-btn {
  width: 100%;
  padding: 8px;
  margin-bottom: 8px;
  background: var(--bg-panel-raised);
  border: 1px solid var(--accent);
  border-radius: 4px;
  color: var(--accent);
  font-family: inherit;
  font-size: 13px;
  font-weight: bold;
  cursor: pointer;
}
.next-unit-btn:hover { background: var(--accent); color: #1a1a1a; }
/* Persistent research entry point (see sidebar.js's footer) -- muted/dashed
   border rather than next-unit-btn's solid accent border, since this is
   informational as often as it's actionable (shows current research %, or
   why nothing's pickable yet) and shouldn't visually compete with the far
   more urgent "units still need orders" cycler right above it. */
.research-btn {
  width: 100%;
  padding: 7px 8px;
  margin-bottom: 8px;
  background: var(--bg-panel-raised);
  border: 1px dashed var(--border);
  border-radius: 4px;
  color: var(--text-main);
  font-family: inherit;
  font-size: 12px;
  cursor: pointer;
}
.research-btn:hover { border-color: var(--accent); color: var(--accent); }
.all-units-moved {
  text-align: center;
  color: var(--text-dim);
  font-size: 12px;
  font-style: italic;
  margin-bottom: 8px;
}

/* --- Unit/city panel action buttons ---
   Rest, Defend, Disband, Build Road, Found City, Cancel Build, the channel
   start/cancel pair, and the production picker's open/close. These were
   unstyled browser default buttons until 2026-08-03. */
.action-btn {
  display: block;
  width: 100%;
  margin-top: 6px;
  padding: 7px 8px;
  background: var(--bg-panel-raised);
  border: 1px solid var(--border);
  border-radius: 4px;
  color: var(--text-main);
  font-family: inherit;
  font-size: 12px;
  cursor: pointer;
}
.action-btn:hover { border-color: var(--accent); color: var(--accent); }
/* The one action the panel is actively recommending -- currently only
   Found City, which is the whole reason a settler exists. */
.action-btn-primary {
  background: var(--accent);
  border-color: var(--accent);
  color: #1a1a1a;
  font-weight: bold;
}
.action-btn-primary:hover { background: var(--accent-hover); border-color: var(--accent-hover); color: #1a1a1a; }
.action-btn-danger { color: #d98a8a; }
.action-btn-danger:hover { border-color: #d98a8a; color: #d98a8a; }

/* --- AI action log --- */
.ai-log-line {
  font-size: 11px;
  color: var(--text-dim);
  padding: 1px 0;
  line-height: 1.4;
  word-break: break-word;
}
.ai-log-strategy {
  color: var(--text-main);
  font-weight: bold;
  margin-bottom: 2px;
}
.view-tech-tree-btn {
  background: var(--bg-panel-raised);
  border: 1px solid var(--border);
  color: var(--accent);
  font-size: 11px;
  padding: 3px 8px;
  border-radius: 3px;
  margin-top: 4px;
  cursor: pointer;
}
.view-tech-tree-btn:hover { border-color: var(--accent); }

/* --- Tech tree overlay (spectator mode) --- */
.techtree-overlay {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.65);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 100;
}
/* Victory celebration fireworks (2026-08-19, user-directed) -- lives inside
   #game-dialog-overlay itself so it shares that overlay's show/hide and
   z-index for free, sitting behind #game-dialog-modal (a later sibling in
   source order, so it paints on top with no z-index of its own needed).
   pointer-events:none so it never blocks clicks on the dialog above it. */
#fireworks-canvas {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
}
.techtree-modal {
  position: relative; /* anchors the .techtree-close-btn X to this modal's own corner, see that rule */
  background: var(--bg-panel);
  border: 1px solid var(--border);
  border-radius: 6px;
  /* 1000px -> 1240px (2026-08-10, user-directed): the new Mystic column
     brought the grid from 3 columns to 4, so the modal grew to keep each
     column's node body from getting cramped. */
  width: min(1240px, 94vw);
  max-height: 88vh;
  padding: 20px 16px 20px 24px;
  /* Flex column, NOT the scroll container itself (2026-08-18, user-
     reported bug fix -- see .techtree-modal-scroll below for why). */
  display: flex;
  flex-direction: column;
}
.techtree-modal h2 {
  margin: 0 0 4px 0;
  color: var(--accent);
}
/* Scrollable body (2026-08-18, user-reported: the close X, and on some
   screens the title, scrolled away with the rest of the content). The
   close button used to be an absolutely-positioned child of .techtree-modal
   while THAT SAME element also had overflow-y: auto -- a common CSS trap:
   an absolutely-positioned descendant of a scrolling positioned ancestor
   scrolls right along with the content, because its offset is resolved
   against the padding box's position in the (scrolled) layout, not against
   the ancestor's on-screen viewport rect. Verified live: scrolling the old
   .techtree-modal moved the close button hundreds of pixels off-screen.
   Fix: .techtree-modal no longer scrolls at all -- it's a flex column, the
   close button is a normal (non-absolute, in-flow) flex item so it's
   simply never inside anything that scrolls, and every modal's actual
   content div gets THIS class instead to become the one thing that
   scrolls, sized via flex: 1 + min-height: 0 (same shape .knowledge-modal/
   #knowledge-content already used, see that rule's own comment). */
.techtree-modal-scroll {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  /* Breathing room so the native scrollbar doesn't sit flush against (and
     overlap) the rightmost content -- see .techtree-modal's own reduced
     right padding, which this makes up for. */
  padding-right: 10px;
}
/* Legacy modals whose content still hangs directly off #id (not yet wrapped
   in a .techtree-modal-scroll div) -- same treatment, just targeted by id
   instead of requiring an HTML wrapper. */
#techtree-content, #reports-content, #launch-options-content {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  padding-right: 10px;
}
/* game-dialog-modal never had a floating close-X (see js/main.js's dialog
   rendering -- its own OK/Cancel/etc. buttons live inside the scrollable
   content), so it never had this bug and keeps scrolling the old way. */
.game-dialog-modal { overflow-y: auto; }
/* The city-automation dialog DOES have a close-X, so it deliberately opts out
   of .game-dialog-modal above and scrolls via its own .techtree-modal-scroll
   body wrapper instead -- otherwise the X would scroll away with the content.
   Same width as the other game dialogs so it doesn't read as a different
   class of window. */
.game-dialog-automation { width: min(420px, 92vw); }
.techtree-close-btn {
  background: transparent;
  border: none;
  color: #d9534f;
  font-size: 22px;
  line-height: 1;
  width: 30px;
  height: 30px;
  border-radius: 50%;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 auto; /* stays exactly 30x30 as a flex item, never stretched/shrunk */
  align-self: flex-end; /* right edge of the flex column, same visual spot as before */
}
.techtree-close-btn:hover { background: rgba(217, 83, 79, 0.18); color: #ff6b64; }
/* No longer position: absolute (see .techtree-modal-scroll's comment above)
   -- both now sit as normal in-flow flex items, pinned to their modal's
   top-right corner via flex-direction: column + align-self: flex-end
   instead of absolute positioning against a scrolling ancestor. Negative
   margin pulls the button up/right to sit flush in the corner rather than
   taking up its own visually-obvious row above the title. */
.techtree-modal > .techtree-close-btn,
.knowledge-modal > .techtree-close-btn {
  margin: -10px -14px -6px 0;
  z-index: 5;
}
/* Tech tree overlay's header row (2026-08-28, user-directed): Skip sits
   directly left of the X, both pinned to the modal's top-right corner as
   one row. Only the tech tree overlay wraps its close button this way --
   .techtree-close-btn's own corner-hugging margin above only fires when
   it's a DIRECT child of .techtree-modal/.knowledge-modal, so nesting it
   one level deeper here (this wrapper) opts it out of that rule on
   purpose; this rule carries the identical corner position instead, just
   on the wrapper. */
.techtree-header-actions {
  display: flex;
  align-items: center;
  gap: 4px;
  align-self: flex-end;
  flex: 0 0 auto;
  margin: -10px -14px -6px 0;
  z-index: 5;
}
.techtree-header-actions .techtree-close-btn { margin: 0; }
/* Icon-only, same circular treatment as .techtree-close-btn (the red X)
   right next to it, just yellow instead of red -- no label text. */
.techtree-skip-btn {
  background: transparent;
  border: none;
  color: #e0b13d;
  font-size: 20px;
  line-height: 1;
  width: 30px;
  height: 30px;
  border-radius: 50%;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 auto;
}
.techtree-skip-btn:hover { background: rgba(224, 177, 61, 0.18); color: #f5cc5c; }
#techtree-content h2 {
  position: sticky;
  top: 0;
  z-index: 1;
  background: var(--bg-panel);
  padding-bottom: 6px;
}
.techtree-layer {
  display: grid;
  grid-template-columns: 76px repeat(4, 1fr);
  gap: 8px;
  padding: 8px 0;
  border-top: 1px solid var(--border);
}
/* A <button> for every per-layer row (the header row's own label stays a
   plain empty div -- see techtree.js's render), so this doubles as a
   button reset. */
.techtree-layer-label {
  color: var(--text-dim);
  font-size: 12px;
  font-weight: bold;
  align-self: start;
  padding: 6px 4px 6px 0;
  background: none;
  border: none;
  font-family: inherit;
  text-align: left;
  cursor: pointer;
  display: flex;
  align-items: center;
  gap: 4px;
}
.techtree-header .techtree-layer-label { cursor: default; }
.techtree-layer-caret {
  display: inline-block;
  width: 0.8em;
  color: var(--text-dim);
}
/* Collapsed layer (2026-08-27, user-directed: Level 0 defaults to this --
   see main.js's lazy default): only the 4 columns hide. The label (and its
   data-avail, still on the parent .techtree-layer) stays put so it's both
   still clickable to re-expand and still found by main.js's "scroll to
   highest available layer" query. */
.techtree-layer-collapsed .techtree-column { display: none; }
.techtree-header { border-top: none; padding-bottom: 4px; }

/* "Right-click for actions" (2026-08-06, user-directed). Once every verb
   moved to the radial map menu, this became the only signpost to the whole
   action set, so it's a real row rather than a dim aside. */
.action-hint {
  margin-top: 8px;
  padding: 6px 8px;
  border: 1px dashed var(--border);
  border-radius: 4px;
  color: var(--text-dim);
  font-size: 11px;
  text-align: center;
}

/* Radial map menu (2026-08-06, user-directed) -- see js/ui/ringmenu.js.
   Replaces the former linear .map-context-menu that floated at the cursor.
   The container spans the whole map area and is absolutely positioned
   against it (#map-context-menu-root now lives inside .map-area, which is
   position:relative), so pill coordinates are just canvas pixels -- no
   viewport math, and the sidebar/menu bar are excluded from the clamp box by
   element geometry rather than by hardcoded sizes. pointer-events:none on the
   container is load-bearing: the map has to stay draggable everywhere the
   pills aren't, and main.js's click-outside dismissal relies on a map
   mousedown still reporting the canvas as its target. */
.map-ring {
  position: absolute;
  inset: 0;
  z-index: 500;
  pointer-events: none;
}
.map-ring-focus {
  position: absolute;
  border-radius: 50%;
  border: 1px solid rgba(230, 190, 110, 0.5);
  background: rgba(230, 190, 110, 0.10);
  box-shadow: 0 0 12px rgba(0, 0, 0, 0.45);
}
.map-ring-item {
  position: absolute;
  transform: translateY(-50%);
  pointer-events: auto;
  box-sizing: border-box;
  /* A flex row so the label span is the only part that shrinks -- the cost
     and shortcut badges keep their natural width instead of being
     ellipsized away first. See ringmenu.js's itemHtml. */
  display: flex;
  align-items: center;
  height: 30px;            /* must match ringmenu.js's PILL_H */
  padding: 0 10px;
  background: var(--bg-panel-raised);
  border: 1px solid var(--border);
  border-radius: 15px;
  color: var(--text-main);
  font-size: 12px;
  font-family: inherit;
  cursor: pointer;
  white-space: nowrap;
  overflow: hidden;
  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.5);
}
/* min-width:0 is load-bearing: a flex item's default min-width:auto refuses
   to shrink below its content, which would push the badges out of the pill
   instead of ellipsizing the label. */
.map-ring-item-label {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
}
/* Labels grow away from the subject, so each side aligns its content against
   the edge that is pinned to the circle. */
.map-ring-item-right { text-align: left; justify-content: flex-start; }
.map-ring-item-left  { text-align: right; justify-content: flex-end; }
.map-ring-item:hover { background: var(--bg-panel); border-color: var(--accent); color: var(--accent); }
/* City-action pills (2026-08-07, user-directed) -- a step lighter than
   --bg-panel-raised's #262b34, so the two columns of a merged unit+city
   ring (see orders.js's mergeUnitCityOptions) read as visibly two different
   KINDS of action at a glance, not just two different labels. Border
   lightened to match, same ratio the base pill keeps between its own
   background and border. */
.map-ring-item-city { background: #333a46; border-color: #454d5c; }
.map-ring-item-city:hover { background: var(--bg-panel); border-color: var(--accent); color: var(--accent); }
/* Danger actions (2026-08-06) -- Cancel*/Disband/Stop Order -- same red as
   sidebar.js's .action-btn-danger. */
.map-ring-item-danger { color: #d98a8a; }
.map-ring-item-danger:hover { background: var(--bg-panel); color: #d98a8a; border-color: #d98a8a; }
/* Keyboard-shortcut badge (2026-08-07, user-directed) -- see ringmenu.js's
   itemHtml. Plain trailing inline text, same nowrap/ellipsis box as the
   label itself. */
.map-ring-item-shortcut {
  margin-left: 6px;
  padding: 1px 5px;
  border-radius: 8px;
  font-size: 10px;
  color: var(--text-dim);
  background: rgba(0, 0, 0, 0.25);
}
/* Cost badge (2026-08-19, user-directed) -- see ringmenu.js's itemHtml.
   Same green/red-against-stockpile convention as buildlist.js's
   costTokenHtml, just a single flat cost rather than a per-resource token
   list (currently only orders.js's Build Bridge pill sets o.cost). */
.map-ring-item-cost {
  margin-left: 6px;
  font-size: 11px;
  font-weight: bold;
  color: #6fbf6f;
}
.map-ring-item-cost-unaffordable { color: #d9695f; }

/* Ring sub-page (2026-08-06) -- the build list and the level-up picker. A
   panel rather than more pills: these are rows of label + cost tokens +
   turn count, which no pill can carry. Anchored beside the subject and
   rendered into the same root as the ring, so it inherits the ring's
   click-outside and Escape dismissal. */
.map-ring-popover {
  position: absolute;
  pointer-events: auto;
  box-sizing: border-box;
  width: 260px;
  overflow-y: auto;
  padding: 8px;
  background: var(--bg-panel-raised);
  border: 1px solid var(--border);
  border-radius: 6px;
  box-shadow: 0 4px 18px rgba(0, 0, 0, 0.55);
}
.map-ring-popover-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  margin-bottom: 6px;
  padding-bottom: 6px;
  border-bottom: 1px solid var(--border);
  color: var(--accent);
  font-size: 13px;
  font-weight: bold;
}
.map-ring-back {
  background: transparent;
  border: 1px solid var(--border);
  border-radius: 3px;
  color: var(--text-dim);
  font-family: inherit;
  font-size: 11px;
  padding: 2px 8px;
  cursor: pointer;
}
.map-ring-back:hover { color: var(--accent); border-color: var(--accent); }
/* The rows themselves are sidebar.js's .build-option, reused verbatim (see
   js/ui/buildlist.js) -- only the type scale needs a nudge for the slightly
   narrower panel. */
.map-ring-popover .build-option { font-size: 12px; }
.map-ring-popover .stat-row { font-size: 12px; }

/* Fallback for a subject whose option list is taller than the map area --
   see ringmenu.js's layout(), mode "list". A plain stacked column; no
   circle is possible at that size. */
.map-ring-list {
  position: absolute;
  pointer-events: auto;
  display: flex;
  flex-direction: column;
  gap: 2px;
  overflow-y: auto;
  padding: 4px;
  background: var(--bg-panel-raised);
  border: 1px solid var(--border);
  border-radius: 4px;
  box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4);
}
.map-ring-item-listed {
  position: static;
  transform: none;
  border: none;
  border-radius: 3px;
  background: transparent;
  box-shadow: none;
  text-align: left;
  justify-content: flex-start;
  min-width: 170px;
  flex-shrink: 0;   /* the list is a column; don't let tall rows squash */
}
.techtree-column-title {
  color: var(--accent);
  font-size: 13px;
  font-weight: bold;
  text-transform: uppercase;
  letter-spacing: 0.5px;
  text-align: center;
  padding-bottom: 4px;
  border-bottom: 1px solid var(--border);
}
.techtree-column { display: flex; flex-direction: column; gap: 6px; }
.techtree-node {
  border: 1px solid var(--border);
  border-radius: 4px;
  padding: 6px 8px;
  font-size: 12px;
  line-height: 1.35;
}
/* A selectable node is a real <button>, unlike every other (div) node state
   -- without an explicit background/color it fell back to the BROWSER's
   default button chrome (near-white), and .techtree-node-name's light
   var(--text-main) on top of that was nearly unreadable (2026-08-03,
   user-reported). Reset it to the same dark panel look every other node
   already has, then let the existing .completed/.researching/.next-pick/
   .locked overrides layer on top exactly as before. */
.techtree-node-selectable {
  display: block;
  width: 100%;
  background: var(--bg-panel-raised);
  color: var(--text-main);
  font-family: inherit;
  text-align: left;
  cursor: pointer;
}
.techtree-node-selectable:hover { border-color: var(--accent); }
.techtree-node-name { font-weight: bold; color: var(--text-main); }
.techtree-node-desc { color: var(--text-dim); font-size: 11px; margin-top: 2px; }
/* Live unit stat block (2026-08-10, user-directed): rendered fresh every
   time from units.js/combat.js (see techtree.js's unitStatsHtml) for any
   tech that unlocks a unit, so it can never go stale the way baked-into-the-
   description numbers used to. Visually distinct from the plain description
   text above it -- a compact stat readout, not prose. */
.techtree-node-unit-stats { color: var(--accent); font-size: 11px; margin-top: 3px; font-weight: 600; }
/* Unit/condition cross-links into the Knowledge Base (2026-08-26, user-
   directed) -- techtree.js's unitStatsHtml/conditionLinksHtml, wired in
   main.js's wireTechTreeKbLinks. Reuses .tile-link's own look (see that
   rule's comment) so "this text jumps somewhere" always reads the same way
   across the whole game; the only addition here is spacing, since these sit
   right after other text rather than alone in their own row. */
.techtree-kb-link { margin-left: 6px; }
.techtree-node-conditions { margin-top: 3px; display: flex; flex-wrap: wrap; gap: 2px 10px; }
.techtree-node-tag { font-size: 10px; text-transform: uppercase; letter-spacing: 0.4px; margin-top: 3px; display: inline-block; }
.techtree-node.completed { border-color: #4a8f5c; background: rgba(74, 143, 92, 0.12); }
.techtree-node.completed .techtree-node-tag { color: #6bc47f; }
.techtree-node.researching { border-color: var(--accent); background: rgba(201, 168, 87, 0.14); }
.techtree-node.researching .techtree-node-tag { color: var(--accent); }
.techtree-node.next-pick { border-color: #6aa0e0; box-shadow: 0 0 0 1px #6aa0e0 inset; }
.techtree-node.next-pick .techtree-node-tag { color: #6aa0e0; }
.techtree-node.locked { opacity: 0.5; }
.techtree-node.locked .techtree-node-tag { color: #b05555; }
/* Far-future tiers (2026-08-04, user-directed): dimmed further than a
   regular locked node, full clarity on hover so it's never permanently
   unreadable -- just out of the way until you're actually planning that far
   ahead. transition makes the hover reveal a deliberate act, not a flicker. */
.techtree-node.techtree-node-far { opacity: 0.22; transition: opacity 0.15s ease; }
.techtree-node.techtree-node-far:hover { opacity: 1; }
/* Reference tech tree (2026-08-26, user-directed) -- the Knowledge Base's
   own "Tech Trees" tab, rendered against a synthetic civ frozen at 0 cities
   forever (see main.js's buildReferenceCiv). Every non-Level-0 tech there is
   permanently "locked" by construction, which is a meaningless signal in a
   page whose whole job is to be READ, not played toward -- so this page (and
   only this page; techtree.js's own isReference flag already keeps
   techtree-node-far from ever being added here at all, see renderNode's
   comment) gets its ordinary .locked dimming turned back off too. */
.techtree-reference .techtree-node.locked { opacity: 1; }

/* "Jump to this tech" target (research-complete modal, 2026-08-10,
   user-directed): a brief pulse so the linked node is easy to spot after the
   screen scrolls to it, fading back to its normal state-based styling. */
@keyframes techtree-node-focus-pulse {
  0%, 100% { box-shadow: 0 0 0 2px var(--accent) inset; }
  50% { box-shadow: 0 0 0 2px transparent inset; }
}
.techtree-node.techtree-node-focused { animation: techtree-node-focus-pulse 0.9s ease-in-out 2; }
/* Reduced motion -- static highlight instead of the pulse, same reasoning
   and same data attribute as .loading-spinner above. */
:root[data-motion-reduced="true"] .techtree-node.techtree-node-focused {
  animation: none;
  box-shadow: 0 0 0 2px var(--accent) inset;
}

/* Prereq/unlock hover highlighting (2026-08-10, user-directed; dimming of
   unrelated nodes removed 2026-08-10, user-directed follow-up -- it caused
   too much visual "flicker" as the cursor moved around): hovering any node
   highlights its prereq chain both directions -- see techtree.js's
   computeRelations/relationKindFor and main.js's mouseenter/mouseleave
   wiring. Indirect (2+ hops) relations get the same color at lower
   opacity, so "how far away" reads at a glance without a second color
   scale. Every OTHER node is left exactly as it already was -- no dimming,
   no transition. Declared after every other state rule so an active hover
   always wins visually over completed/next-pick/etc. */
.techtree-node.techtree-node-relation-self { border-color: var(--accent); box-shadow: 0 0 0 2px var(--accent) inset; }
.techtree-node.techtree-node-relation-ancestor { border-color: #8a7fd6; box-shadow: 0 0 0 1px #8a7fd6 inset; }
.techtree-node.techtree-node-relation-descendant { border-color: #e0a050; box-shadow: 0 0 0 1px #e0a050 inset; }
.techtree-node.techtree-node-relation-indirect { opacity: 0.6; }

.reports-modal { width: min(800px, 94vw); }
/* AI Tech Trees (2026-08-10, user-directed): the embedded tree reuses
   .techtree-layer's 4-column grid (civic/building/military/mystic), which
   .reports-modal's normal 800px (sized for the 720px-wide charts/log)
   crams badly -- widened to match .techtree-modal's own 1240px whenever
   this specific report is showing. Toggled in main.js's redraw() based on
   viewState.reportView, since every other report type still wants the
   narrower default. */
.reports-modal.reports-modal-wide { width: min(1240px, 94vw); }
/* Keyboard Shortcuts window (2026-08-07, user-directed) -- reuses
   .techtree-modal for the backdrop/frame, same as .reports-modal. */
.shortcuts-modal { width: min(520px, 94vw); }
/* Tutorial window -- reuses .techtree-modal for the backdrop/frame (same as
   .shortcuts-modal above) and credits.js's .credits-title/-heading/-para
   classes for its content (see js/main.js's openTutorial). Widened twice,
   960px then this (2026-09-13, user-directed) as tutorial.txt itself grew
   from a handful of short sections into a full walkthrough -- now matches
   .techtree-modal's own 1240px default (no override needed) rather than
   trailing it. */
/* The credits crawl centers everything (it's a scrolling marquee) and sizes
   its type for a big projected scroll, but a static document reads better
   smaller and left-aligned -- override both, scoped to #tutorial-content so
   the credits crawl itself is untouched. */
#tutorial-content .credits-title,
#tutorial-content .credits-heading,
#tutorial-content .credits-para { text-align: left; }
#tutorial-content .credits-title { font-size: 22px; }
#tutorial-content .credits-heading { font-size: 15px; }
#tutorial-content .credits-para { font-size: 14px; }
.shortcuts-modal #fullscreen-toggle-btn { width: 100%; margin: 12px 0 4px 0; }
.shortcuts-section { margin-top: 16px; }
.shortcuts-section-title {
  color: var(--accent);
  font-size: 12px;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  border-bottom: 1px solid var(--border);
  padding-bottom: 4px;
  margin-bottom: 6px;
}
.shortcuts-row {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 4px 0;
  color: var(--text-main);
  font-size: 13px;
}
.shortcuts-key {
  flex: 0 0 auto;
  min-width: 90px;
  padding: 2px 8px;
  border: 1px solid var(--border);
  border-radius: 4px;
  background: var(--bg-panel-raised);
  color: var(--text-dim);
  font-size: 11px;
  text-align: center;
  white-space: nowrap;
}
/* Kingdom Marches Knowledge Base (KMKB) -- js/ui/knowledgebase.js.
   A separate overlay class from .techtree-overlay (not just a reused one)
   specifically so .knowledge-overlay-ingame can narrow it to stop at the
   sidebar's edge -- every OTHER .techtree-overlay-based modal deliberately
   covers the full viewport, but this one was explicitly required not to
   overlap #sidebar. Base rule still covers the full viewport (right: 0),
   matching every other modal, for the title-screen case where there's no
   sidebar to avoid at all. */
.knowledge-overlay {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.65);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 100;
}
.knowledge-overlay.knowledge-overlay-ingame {
  right: 280px; /* matches .sidebar's own width -- never overlaps it */
}
.knowledge-modal {
  position: relative; /* anchors the .techtree-close-btn X to this modal's own corner */
  background: var(--bg-panel);
  border: 1px solid var(--border);
  border-radius: 6px;
  /* ~10% smaller (2026-08-17, user-directed) than every other
     .techtree-overlay-based modal's own 1240px/88vh -- both the pixel cap
     AND the percentage cap scaled down together so this stays proportionally
     smaller on both large and small viewports, not just large ones. */
  width: min(1260px, 85%);
  height: 79vh;
  padding: 20px 16px 20px 24px;
  display: flex;
  flex-direction: column;
  overflow: hidden; /* .kb-list-pane/.kb-profile-pane scroll themselves */
}
/* #knowledge-content is .knowledge-modal's actual flex child (the close
   button is the only sibling) -- WITHOUT this rule it renders as a plain
   block sized to its own full unclipped content, so .kb-body's `flex: 1;
   min-height: 0;` below has no constrained parent to size against and
   .kb-list-pane/.kb-profile-pane's overflow-y: auto never has anything to
   actually clip (2026-08-17, user-reported: KMKB windows didn't scroll).
   Mirrors .knowledge-modal's own flex-column shape one level down. */
#knowledge-content { flex: 1; min-height: 0; display: flex; flex-direction: column; }
.kb-header { flex: 0 0 auto; margin-bottom: 8px; }
.kb-header h2 { margin: 0; }
/* Shared by both KMKB pages (2026-08-16, user-directed: Conditions reuses
   the Units page's exact list+profile layout instead of its own grid, for
   visual consistency) -- .kb-list-pane/.kb-list-btn/etc. are generic, not
   unit-specific; only .kb-unit-portrait below stays unit-only (conditions
   show an icon glyph instead, styled separately). */
.kb-body { flex: 1; min-height: 0; display: flex; gap: 16px; }
.kb-list-pane {
  flex: 0 0 240px;
  overflow-y: auto;
  border-right: 1px solid var(--border);
  padding-right: 12px;
}
.kb-list-group { margin-bottom: 14px; }
.kb-list-group-label {
  color: var(--accent);
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  border-bottom: 1px solid var(--border);
  padding-bottom: 3px;
  margin-bottom: 4px;
}
.kb-list-btn {
  display: flex;
  align-items: center;
  gap: 8px;
  width: 100%;
  text-align: left;
  background: none;
  border: 1px solid transparent;
  border-radius: 4px;
  padding: 5px 6px;
  color: var(--text-main);
  font-size: 13px;
  cursor: pointer;
}
.kb-list-btn:hover { background: var(--bg-panel-raised); }
.kb-list-btn-selected { background: var(--bg-panel-raised); border-color: var(--accent); }
.kb-list-btn-symbol { width: 18px; text-align: center; color: var(--text-dim); }
/* Terrain page's list marker: a color chip standing in for the glyph the
   Units/Structures lists use in .kb-list-btn-symbol's slot -- terrain has
   no symbol, but it does have a real color (terrain.js's own `color`, the
   same value render.js paints the tile with). Same 18px column so both
   list styles align identically. */
.kb-list-btn-swatch {
  width: 18px;
  height: 14px;
  flex: 0 0 auto;
  border-radius: 2px;
  border: 1px solid rgba(0, 0, 0, 0.45);
}
/* The Terrain profile's preview shares .kb-unit-portrait's box, but its
   art is a TILE, not a figure on transparency -- so it gets a hard edge
   and fills its frame rather than floating in it. */
.kb-terrain-portrait {
  width: 128px;
  height: 128px;
  flex: 0 0 auto;
  border: 1px solid var(--border);
  border-radius: 4px;
  image-rendering: auto;
}
.kb-profile-pane { flex: 1; min-width: 0; overflow-y: auto; padding-right: 10px; }
.kb-profile-empty {
  color: var(--text-dim);
  font-style: italic;
  padding-top: 40px;
  text-align: center;
}
.kb-profile-empty-inline { color: var(--text-dim); font-size: 12px; font-style: italic; }
.kb-profile-header { display: flex; gap: 14px; align-items: flex-start; margin-bottom: 10px; }
.kb-unit-portrait {
  flex: 0 0 auto;
  width: 96px;
  height: 96px;
  border: 1px solid var(--border);
  border-radius: 6px;
  background: var(--bg-panel-raised);
}
.kb-profile-header h2 { margin: 0 0 2px 0; }
.kb-profile-subline { color: var(--text-dim); font-size: 12px; margin-bottom: 6px; }
.kb-chip-row { display: flex; flex-wrap: wrap; gap: 6px; margin: 4px 0 10px 0; }
.kb-chip {
  display: inline-block;
  padding: 2px 8px;
  border: 1px solid var(--border);
  border-radius: 12px;
  font-size: 11px;
  color: var(--text-dim);
  background: var(--bg-panel-raised);
}
.kb-chip-action { color: var(--accent); border-color: var(--accent); }
/* A unit profile's "Available Actions" chip that cross-links to the Actions
   page (2026-08-28, user-directed) -- rendered as a <button> rather than a
   <span> like a plain, unlinked chip, so this resets the button chrome back
   to matching .kb-chip's own pill look, then adds just a pointer cursor and
   a hover highlight on top. */
.kb-chip-link {
  font: inherit;
  cursor: pointer;
}
.kb-chip-link:hover { background: var(--accent); color: var(--bg-panel); }
.kb-tech-rel {
  font-size: 12px;
  color: var(--text-main);
  border-top: 1px solid var(--border);
  padding: 6px 0;
}
.kb-tech-rel-label {
  display: inline-block;
  min-width: 90px;
  color: var(--text-dim);
  text-transform: uppercase;
  font-size: 10px;
  letter-spacing: 0.05em;
}
.kb-tech-badge { color: var(--text-dim); font-size: 10px; margin-left: 4px; }
/* A unit's own default properties that map to a Conditions-page entry
   (2026-08-16, user-directed) -- e.g. Wizard's "Burning — 5% chance to
   inflict on hit". A real <button>, not a styled <div>, so it's keyboard-
   reachable like every other clickable KMKB row. */
.kb-condition-link {
  display: flex;
  align-items: center;
  gap: 8px;
  width: 100%;
  text-align: left;
  background: none;
  border: 1px solid var(--border);
  border-radius: 4px;
  padding: 6px 8px;
  margin-bottom: 4px;
  color: var(--text-main);
  font-size: 12px;
  cursor: pointer;
}
.kb-condition-link:hover { border-color: var(--accent); background: var(--bg-panel-raised); }
.kb-condition-link-icon { font-size: 16px; flex: 0 0 auto; }
.kb-condition-link-text { flex: 1; }
.kb-condition-link-arrow { color: var(--accent); font-size: 11px; flex: 0 0 auto; }
.kb-back-btn {
  display: inline-block;
  background: none;
  border: 1px solid var(--border);
  border-radius: 4px;
  padding: 4px 10px;
  margin-bottom: 12px;
  color: var(--text-dim);
  font-size: 12px;
  cursor: pointer;
}
.kb-back-btn:hover { border-color: var(--accent); color: var(--accent); }
.kb-condition-profile-icon {
  flex: 0 0 auto;
  width: 96px;
  height: 96px;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 52px;
  border: 1px solid var(--border);
  border-radius: 6px;
  background: var(--bg-panel-raised);
}
/* white-space:pre-line (2026-08-17) -- the Defense stat's own writeup uses
   a literal blank line to separate the formula from its surrounding prose
   (see knowledgebase.js's STAT_DESCRIPTIONS); this is the shared class both
   Conditions and Stats profile descriptions use, so it renders there too. */
.kb-condition-profile-desc { color: var(--text-dim); font-size: 13px; line-height: 1.5; white-space: pre-line; }
/* A unit profile's stat-row label, cross-linking to that stat's own KMKB
   entry (2026-08-17, user-directed) -- underlined-dotted rather than a
   bordered pill like .kb-condition-link, since this sits inline inside an
   existing .stat-row rather than being its own full-width row. */
.kb-stat-link {
  background: none;
  border: none;
  padding: 0;
  color: var(--text-dim);
  font-size: 13px;
  text-decoration: underline dotted;
  text-underline-offset: 2px;
  cursor: pointer;
}
.kb-stat-link:hover { color: var(--accent); }
/* Live Attack-vs-Defense simulator (2026-08-17, user-directed) -- embedded
   on the Attack/Defense stat pages, see knowledgebase.js's
   COMBAT_SIM_HTML/wireCombatSimulator. */
.kb-sim {
  margin-top: 14px;
  padding: 12px 14px;
  border: 1px solid var(--border);
  border-radius: 6px;
  background: var(--bg-panel-raised);
}
.kb-sim-row { display: flex; align-items: center; gap: 10px; margin-bottom: 8px; }
.kb-sim-label { flex: 0 0 70px; color: var(--text-dim); font-size: 13px; }
.kb-sim-btn {
  width: 26px;
  height: 26px;
  border: 1px solid var(--border);
  border-radius: 4px;
  background: var(--bg-panel);
  color: var(--text-main);
  font-size: 15px;
  line-height: 1;
  cursor: pointer;
}
.kb-sim-btn:hover { border-color: var(--accent); color: var(--accent); }
.kb-sim-value { flex: 0 0 32px; text-align: center; font-weight: bold; color: var(--text-main); }
.kb-sim-result { margin-top: 10px; border-top: 1px solid var(--border); padding-top: 8px; }

.reports-subtitle {
  font-size: 12px;
  color: var(--text-dim);
  margin: 4px 0 0 0;
}
.reports-legend {
  display: flex;
  flex-wrap: wrap;
  gap: 12px;
  margin: 8px 0 12px 0;
}
.reports-legend-item {
  display: flex;
  align-items: center;
  gap: 6px;
  font-size: 12px;
  color: var(--text-main);
}
.reports-legend-swatch {
  display: inline-block;
  width: 10px;
  height: 10px;
  border-radius: 50%;
  flex: none;
}
.reports-chart-wrap { position: relative; }
.reports-chart-wrap svg { display: block; }
.reports-tooltip {
  position: absolute;
  pointer-events: none;
  background: var(--bg-panel-raised);
  border: 1px solid var(--border);
  border-radius: 4px;
  padding: 6px 8px;
  font-size: 11px;
  color: var(--text-main);
  min-width: 120px;
  z-index: 5;
}
.reports-tooltip-title { color: var(--text-dim); margin-bottom: 4px; }
.reports-tooltip-row { display: flex; align-items: center; gap: 6px; }
.reports-tooltip-value { margin-left: auto; font-weight: bold; }

/* --- AI Action Log report screen --- */
.ai-action-log-controls {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  margin: 8px 0 10px 0;
  font-size: 12px;
  color: var(--text-main);
}
.ai-action-log-controls select {
  background: var(--bg-panel-raised);
  border: 1px solid var(--border);
  color: var(--text-main);
  border-radius: 3px;
  padding: 3px 6px;
  font-family: inherit;
  font-size: 12px;
  margin-left: 4px;
}
.ai-action-log-controls button {
  background: var(--bg-panel-raised);
  border: 1px solid var(--border);
  color: var(--accent);
  padding: 4px 10px;
  border-radius: 3px;
  cursor: pointer;
  font-size: 12px;
  font-family: inherit;
}
.ai-action-log-controls button:hover { border-color: var(--accent); }
.ai-action-log-controls button:disabled { color: var(--text-dim); cursor: default; opacity: 0.6; }
.ai-action-log-summary { font-size: 11px; color: var(--text-dim); margin-bottom: 6px; }
.ai-action-log-list {
  max-height: 55vh;
  overflow-y: auto;
  border: 1px solid var(--border);
  border-radius: 4px;
  padding: 4px;
}
.ai-action-log-row {
  display: flex;
  align-items: baseline;
  gap: 8px;
  font-size: 11px;
  line-height: 1.4;
  padding: 3px 6px;
  border-left: 3px solid var(--border);
}
.ai-action-log-row:nth-child(odd) { background: rgba(255, 255, 255, 0.03); }
.ai-action-log-turn { color: var(--text-dim); flex: none; font-variant-numeric: tabular-nums; }
.ai-action-log-civ { flex: none; font-weight: bold; min-width: 70px; }
.ai-action-log-text { color: var(--text-main); word-break: break-word; }
.ai-action-log-more-btn {
  display: block;
  width: 100%;
  margin-top: 8px;
  background: var(--bg-panel-raised);
  border: 1px solid var(--border);
  color: var(--text-main);
  padding: 6px;
  border-radius: 3px;
  cursor: pointer;
  font-size: 12px;
  font-family: inherit;
}
.ai-action-log-more-btn:hover { border-color: var(--accent); }

/* --- In-game dialog (replaces browser confirm/prompt/alert) --- */
.game-dialog-modal { width: min(420px, 92vw); }
.game-dialog-modal p { color: var(--text-main); font-size: 13px; line-height: 1.5; }
.game-dialog-input {
  width: 100%;
  background: var(--bg-panel-raised);
  border: 1px solid var(--border);
  border-radius: 3px;
  color: var(--text-main);
  font-family: inherit;
  font-size: 13px;
  padding: 6px 8px;
  margin: 4px 0 14px 0;
}
.game-dialog-input:focus { outline: none; border-color: var(--accent); }
/* Turn-end guard's list of things the player hasn't resolved yet. */
.game-dialog-list {
  color: var(--text-main);
  font-size: 13px;
  line-height: 1.6;
  margin: 4px 0 14px 0;
  padding-left: 20px;
}
/* Idle-city default note on the turn-end guard (2026-08-12, user-directed)
   -- see dialog.js's confirmEndTurn kind. */
.game-dialog-hint { color: var(--text-dim); font-size: 12px; margin: -8px 0 14px 0; }
.game-dialog-actions { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: 8px; }
/* Single centered button with breathing room above it -- the "Welcome Back!"
   reward dialog (see dialog.js's welcomeBack kind), whose lone Collect button
   used to sit flush against the last resource row, right-aligned. */
.game-dialog-actions-centered { justify-content: center; margin-top: 20px; }
/* N-way tech choice (2026-08-05, user-directed) -- see dialog.js's
   "chooseTech" kind. Vertical list of full-width buttons instead of
   .game-dialog-actions' row layout, since the option count varies. */
.game-dialog-choices { display: flex; flex-direction: column; gap: 8px; margin: 4px 0 4px 0; }
.game-dialog-choice-label { font-weight: 600; }
.game-dialog-choice-desc { font-size: 11px; color: var(--text-dim); margin-top: 2px; }
/* Tech-researched / unit-built announcements (2026-08-06, user-directed). */
.game-dialog-tech-name { font-weight: 600; font-size: 14px; color: var(--accent); }
.game-dialog-unlocked-label { font-weight: 600; margin: 8px 0 2px 0; }
.game-dialog-primary { background: var(--accent); border-color: var(--accent); color: #1a1a1a; }
.game-dialog-primary:hover { background: var(--accent-hover); }
/* Destructive confirm (2026-08-04, user-directed) -- e.g. Disband Unit --
   same red as .action-btn-danger's text, but filled, so the dialog's own
   weight signals "this is the dangerous one" the way action-btn-primary's
   gold fill already does for the recommended one. */
.game-dialog-danger { background: #d9695f; border-color: #d9695f; color: #1a1a1a; }
.game-dialog-danger:hover { background: #e07d73; }

/* Victory stats screen (2026-08-20, user-directed): a grander, more
   ceremonial treatment than the plain utilitarian dialogs above -- wider
   modal, display-face title with a glow, and the same per-kingdom gilded
   border-image the sidebar uses (see the .sidebar.race-* rules above --
   .game-dialog-victory.race-<raceId> is set by main.js's redraw() the same
   way sidebar.js sets .sidebar's race-* class, keyed off the WINNING civ's
   raceId, not the human player's). border-radius has no effect once
   border-image-source is set (same as the sidebar), so it's omitted here
   rather than left in as dead weight. */
.game-dialog-victory {
  width: min(600px, 92vw);
  border-width: 18px;
  border-style: solid;
  border-image-slice: 20;
  border-image-repeat: round;
  text-align: center;
}
.victory-title {
  font-family: "Cinzel", Georgia, serif;
  font-size: 40px;
  font-weight: 900;
  letter-spacing: 2px;
  margin: 4px 0 2px 0;
  color: var(--accent);
  text-shadow: 0 0 18px rgba(201, 168, 87, 0.55), 0 2px 2px rgba(0, 0, 0, 0.6);
}
.victory-subtitle {
  font-family: "Cinzel", Georgia, serif;
  font-size: 15px;
  letter-spacing: 1px;
  text-transform: uppercase;
  color: var(--text-dim);
  margin: 0 0 16px 0;
}
.victory-divider {
  height: 1px;
  margin: 14px 0;
  background: linear-gradient(90deg, transparent, var(--accent), transparent);
}
.game-dialog-victory .stat-row { font-size: 15px; padding: 6px 0; text-align: left; }
.game-dialog-victory .stat-row span:last-child { color: var(--accent); }
.victory-rivals-label {
  font-family: "Cinzel", Georgia, serif;
  font-size: 13px;
  letter-spacing: 1px;
  text-transform: uppercase;
  color: var(--text-dim);
  text-align: left;
  margin: 0 0 8px 0;
}
.victory-rivals-list { display: flex; flex-direction: column; gap: 4px; }
.victory-rival-row {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 4px 0;
  font-size: 13px;
  text-align: left;
}
.victory-rival-swatch {
  width: 10px;
  height: 10px;
  border-radius: 50%;
  flex: none;
  box-shadow: 0 0 4px rgba(0, 0, 0, 0.5);
}
.victory-rival-label { flex: 1; color: var(--text-main); }
.victory-rival-status { color: var(--text-dim); font-variant-numeric: tabular-nums; }
.victory-rival-eliminated .victory-rival-label { color: var(--text-dim); text-decoration: line-through; }
.victory-rival-eliminated .victory-rival-status { color: #d9695f; }
.game-dialog-victory .game-dialog-actions { justify-content: center; margin-top: 18px; }
.game-dialog-victory .game-dialog-primary { padding: 10px 28px; font-size: 14px; letter-spacing: 0.5px; }

/* AI turn-processing banner (2026-08-04, user-directed): End Turn used to
   resolve the whole round in one blocking synchronous pass with nothing
   painted until it was over -- however long that took, the player just sat
   looking at their last move with no indication anything was happening.
   advanceTurn() now yields between civs (see main.js) specifically so this
   can actually paint. A floating banner, not a modal overlay -- this is
   status, not something requiring acknowledgement, and shouldn't block
   input during the brief per-civ pause. */
.turn-progress-banner {
  position: fixed;
  top: 16px;
  left: 50%;
  transform: translateX(-50%);
  z-index: 500;
  padding: 10px 22px;
  background: rgba(20, 22, 27, 0.92);
  border: 1px solid var(--accent);
  border-radius: 6px;
  color: var(--accent);
  font-size: 14px;
  font-weight: bold;
  letter-spacing: 0.3px;
  pointer-events: none;
}
/* --- Launch options modal (reuses .techtree-overlay for the backdrop) --- */
.launch-options-modal {
  /* A fixed cap rather than the old viewport-proportional
     min(max(64vw, 640px), 94vw). That formula grew the modal with the
     screen -- 819px on a 1280px viewport -- which only ever bought more
     empty space, since the content is four fixed-size cards and not a
     table. 680px seats exactly two ~260px cascade columns (see
     .launch-grid) with room for the modal's own padding and the scrollbar
     gutter, and nothing gains from a third. The 94vw term is still the
     overflow guard for a narrow window, where it resolves to a single
     column instead. */
  width: min(680px, 94vw);
  max-height: 88vh;
  overflow-y: auto;
}
.launch-title {
  margin: 0 0 12px 0;
  font-family: "Cinzel", Georgia, serif;
  font-size: 20px;
  color: var(--accent);
  letter-spacing: 0.06em;
}
/* CASCADING LAYOUT (2026-08-26, user-directed: "the game options screen is
   too wide for mobile with 2 columns, and looks strange in desktop (lots of
   empty space). change to cascading model?")
   -----------------------------------------------------------------------
   Was a rigid 2x2 grid (grid-template-columns: 1fr 1fr + two explicit rows
   + grid-auto-flow: column), which broke in both directions:

   - DESKTOP: explicit rows make every card in a row as tall as the tallest
     card in it. World is ~400px and Mode is ~90px, so row 1 became 400px
     tall and Single Player -- row 2 of column 1 -- was pushed 300px down
     the page, leaving a hole that size in the middle of the left column.
     Removing the Audio section (2026-08-26) took the grid from four cards
     to three and made the degenerate case the normal one.
   - MOBILE: the two columns were unconditional. On a 375px screen that left
     each ~190px wide, which the World card's own content then overflowed
     straight past the right edge of the modal (measured: 543px right edge
     inside a 353px modal).

   A CSS multi-column cascade fixes both with no media query and no fixed
   count: the browser fits as many `columns`-wide columns as the container
   can take -- two in the 680px modal, ONE on a phone -- and flows the cards
   down one and into the next. break-inside: avoid is what keeps a card
   whole instead of splitting it across the fold; the bottom margin (rather
   than a grid `gap`) is what spaces them, since columns have no row gap.

   The spectator toggle still needs no special handling: whichever of Single
   Player / Races in Play is display:none simply isn't in the flow, and the
   cascade re-balances around the other. */
.launch-grid {
  columns: 260px;
  column-gap: 18px;
}
.launch-section {
  break-inside: avoid;
  margin: 0 0 12px;
  padding: 12px 14px;
  border: 1px solid var(--border);
  border-radius: 6px;
  background: var(--bg-panel-raised);
}
.launch-section-label {
  font-size: 10px;
  text-transform: uppercase;
  letter-spacing: 0.14em;
  color: var(--text-dim);
  margin-bottom: 9px;
}
.launch-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  margin-bottom: 7px;
  font-size: 12px;
  color: var(--text-main);
}
.launch-row:last-child { margin-bottom: 0; }
.launch-row-check { cursor: pointer; }
/* min-width: 0 overrides the flex-item default of min-width: auto, which
   otherwise refuses to shrink a <select>/<input> below its own content's
   intrinsic width -- a native <select>'s rendered width varies by OS/
   browser (2026-08-26, user-reported: content spilling past the card edge
   on some environments this test browser didn't reproduce), so without
   this a wide-rendered dropdown could hold its row open past the card's
   ~260px inner width instead of actually shrinking to fit it. */
.launch-row select,
.launch-row input[type="text"] {
  flex: 1;
  min-width: 0;
  max-width: 260px;
  background: var(--bg-panel-raised);
  border: 1px solid var(--border);
  border-radius: 3px;
  color: var(--text-main);
  font-family: inherit;
  font-size: 12px;
  padding: 4px 6px;
}
.launch-row select:focus,
.launch-row input[type="text"]:focus { outline: none; border-color: var(--accent); }
.launch-row input[type="checkbox"] { accent-color: var(--accent); margin: 0; }
/* Game Speed slider (2026-08-06, user-directed) -- the percentage label
   sits to the right, inside the same flex row as the slider itself. */
.launch-row-slider {
  display: flex;
  align-items: center;
  gap: 8px;
  flex: 1;
  min-width: 0;
  max-width: 260px;
}
/* min-width: 0 here too -- a <input type=range> has its own non-zero
   intrinsic min-content width in some browsers, which without this can
   refuse to shrink the same way a <select> can (see .launch-row's own
   min-width: 0 note just above). */
.launch-row-slider input[type="range"] {
  flex: 1;
  min-width: 0;
  accent-color: var(--accent);
}
.launch-row-slider span {
  min-width: 34px;
  font-size: 11px;
  text-align: right;
  font-variant-numeric: tabular-nums;
}
.launch-hint {
  margin: 6px 0 0 0;
  font-size: 10px;
  line-height: 1.45;
  color: var(--text-dim);
}
/* Race checklist: a wrapping grid rather than the old single nowrap row,
   which is what capped it at "however many fit across the toolbar". */
.launch-race-list {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 6px 12px;
}
.launch-race-item {
  display: flex;
  align-items: center;
  gap: 6px;
  font-size: 13px;
  color: var(--text-main);
  cursor: pointer;
}
.launch-race-item input[type="checkbox"] { accent-color: var(--accent); margin: 0; }

.launch-actions {
  padding-top: 16px;
  border-top: 1px solid var(--border);
}
.launch-start-btn {
  width: 100%;
  padding: 11px;
  background: var(--accent);
  border: none;
  border-radius: 4px;
  color: #1a1a1a;
  font-family: "Cinzel", Georgia, serif;
  font-size: 15px;
  font-weight: bold;
  letter-spacing: 0.08em;
  cursor: pointer;
}
.launch-start-btn:hover { background: var(--accent-hover); }
/* Build stamp -- date/time/build number under the Start Game button
   (2026-08-03, user-directed). Purely informational. */
/* Build stamp -- lower-right corner of the base title screen (2026-08-07,
   user-directed: moved out from under the Start Game button inside the
   Game Options modal). .title-main is already position:relative (it's the
   flex:1 area holding the bg image + logo), so this just pins to its own
   corner independent of that content. */
.title-build-stamp {
  position: absolute;
  right: 14px;
  bottom: 10px;
  z-index: 1;
  font-size: 11px;
  color: var(--text-dim);
  text-shadow: 0 1px 3px rgba(0, 0, 0, 0.7);
}
/* "View Credits" -- lower-left corner of the base title screen (2026-08-17,
   user-directed: moved out of the Game Options modal's launch-actions row,
   where .launch-credits-btn styled it as a full-width row under Start
   Game). Mirrors title-build-stamp's own corner-pin treatment on the
   opposite side, but this is a real, clickable control sitting directly on
   the artwork rather than a passive label -- kept as a bordered button
   (same subtle-until-hovered shape .launch-credits-btn had) with a text
   shadow for legibility over the background image, matching
   title-build-stamp's own shadow. */
.title-credits-btn {
  position: absolute;
  left: 14px;
  bottom: 10px;
  z-index: 1;
  padding: 6px 14px;
  background: rgba(14, 16, 20, 0.35);
  border: 1px solid var(--border);
  border-radius: 4px;
  color: var(--text-dim);
  font-family: "Cinzel", Georgia, serif;
  font-size: 12px;
  letter-spacing: 0.08em;
  cursor: pointer;
  text-shadow: 0 1px 3px rgba(0, 0, 0, 0.7);
  transition: border-color 0.2s, color 0.2s;
}
.title-credits-btn:hover { border-color: var(--accent); color: var(--accent); }

/* --- Credits crawl (2026-08-07, user-directed) --- */
.credits-overlay {
  position: fixed;
  inset: 0;
  z-index: 110;
  background: rgba(6, 7, 9, 0.94);
  display: flex;
  justify-content: center;
}
.credits-close-btn {
  position: absolute;
  top: 20px;
  right: 24px;
  z-index: 1;
}
.credits-viewport {
  position: relative;
  width: min(720px, 90vw);
  height: 100%;
  overflow: hidden;
  -webkit-mask-image: linear-gradient(to bottom, transparent 0, black 10%, black 90%, transparent 100%);
  mask-image: linear-gradient(to bottom, transparent 0, black 10%, black 90%, transparent 100%);
}
.credits-content {
  position: absolute;
  left: 0;
  right: 0;
  will-change: transform;
}
.credits-title {
  margin: 0 0 10px 0;
  font-family: "Cinzel", Georgia, serif;
  color: var(--accent);
  text-align: center;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  font-size: clamp(24px, 4vw, 36px);
}
.credits-heading {
  margin: 44px 0 10px 0;
  font-family: "Cinzel", Georgia, serif;
  color: var(--accent);
  text-align: center;
  text-transform: uppercase;
  letter-spacing: 0.12em;
  font-size: clamp(15px, 2.2vw, 20px);
}
.credits-para {
  margin: 0 0 8px 0;
  text-align: center;
  color: var(--text-main);
  line-height: 1.7;
}
.credits-para a { color: var(--accent-hover); }
/* [Label](kb:view:id) cross-links (js/ui/credits.js's KB_LINK_RE, wired up
   by js/main.js's openTutorial/jumpToKnowledgeFromTutorial) -- dotted
   underline, same convention as the Knowledge Base's own .kb-stat-link, so
   an in-app jump reads differently from the plain http(s) auto-links above
   (solid underline, actually leaves the page). */
.kb-link {
  color: var(--accent-hover);
  text-decoration: underline dotted;
  text-underline-offset: 2px;
  cursor: pointer;
}
.kb-link:hover { color: var(--accent); }


/* Treasure Chest result dialog (see dialog.js's treasureFound kind): one row per
   treasure, icon on the left. */
.treasure-list { display: flex; flex-direction: column; gap: 8px; margin: 6px 0 4px 0; }
.treasure-row { display: flex; align-items: flex-start; gap: 10px; color: var(--text-main); font-size: 13px; line-height: 1.45; }
.treasure-row .resource-icon { flex: 0 0 auto; width: 20px; height: 20px; margin-top: 1px; }
.treasure-icon { flex: 0 0 20px; width: 20px; text-align: center; font-size: 17px; line-height: 1.2; }
.treasure-text { flex: 1 1 auto; }

/* Unit panel blocks (see sidebar.js's unitBlockHtml): Conditions, Veteran
   Bonuses and Items each get their own labelled, stacked list. */
.unit-block { margin: 10px 0 4px 0; }
.unit-block-title {
  font-size: 11px; letter-spacing: 0.08em; text-transform: uppercase;
  color: var(--text-dim); border-bottom: 1px solid var(--border); padding-bottom: 3px; margin-bottom: 4px;
}
.unit-block-list { list-style: none; margin: 0; padding: 0; }
.unit-block-row { font-size: 12px; line-height: 1.45; padding: 2px 0 2px 10px; border-left: 3px solid var(--border); margin-bottom: 2px; color: var(--text-main); }
.unit-block-good { border-left-color: #6fbf6f; }
.unit-block-bad { border-left-color: #d9695f; }

/* Unique item found (data/items.js `unique`): a static gold-edged row -- no animation. */
.treasure-row-unique { padding: 6px 8px; border: 1px solid var(--accent); border-radius: 6px; background: rgba(201, 168, 87, 0.10); }
.treasure-row-unique .treasure-text { color: var(--accent-hover); font-weight: 600; }

/* ---------------------------------------------------------------------------
   STORY (single player) -- js/ui/story.js renders; doc/story_bible.md designs.
   A scene is one line at a time in #game-dialog-modal, framed in the current
   speaker's kingdom gilding (the .game-dialog-story.race-* rules above share
   the sidebar's border-image). Narration and the Marchstone get their own
   panels. Barks are small corner cards that fade in and out -- opacity only,
   never flashing (photosensitivity rule).
   --------------------------------------------------------------------------- */
.game-dialog-story {
  width: min(700px, 94vw);
  position: relative;
  border-width: 18px;
  border-style: solid;
  border-image-slice: 20;
  border-image-repeat: round;
  padding: 16px 18px 12px;
}
.story-header {
  font-family: "Cinzel", Georgia, serif;
  font-size: 12px;
  letter-spacing: 1px;
  text-transform: uppercase;
  color: var(--text-dim);
  margin-bottom: 10px;
}
.story-line { display: flex; gap: 16px; align-items: flex-start; min-height: 160px; }
/* Speakers alternate sides within a scene (js/ui/story.js speakerSide). */
.story-line.story-side-right { flex-direction: row-reverse; }
.story-body { flex: 1; min-width: 0; }
/* Name, with the honorific stacked beneath it; always left-aligned, even
   when the portrait is on the right. */
.story-nameplate { display: flex; flex-direction: column; align-items: flex-start; gap: 1px; margin-bottom: 8px; }
.story-name {
  font-family: "Cinzel", Georgia, serif;
  font-size: 15px;
  font-weight: 700;
  letter-spacing: 0.5px;
  text-transform: uppercase;
  color: var(--accent);
}
.story-title { font-style: italic; font-size: 12.5px; line-height: 1.3; color: var(--text-dim); }
.game-dialog-story .story-text { font-size: 16px; line-height: 1.55; margin: 0; }
.story-text em, .story-bark-text em { color: var(--accent-hover); }
.story-text strong, .story-bark-text strong { font-weight: 700; color: var(--text-main); }
/* Word-by-word reveal: words are laid out from the start (no reflow) and
   fade in; opacity only, never flashing (photosensitivity rule). */
.story-revealing .story-w { opacity: 0; transition: opacity 0.12s ease-out; }
.story-revealing .story-w.on { opacity: 1; }
:root[data-motion-reduced="true"] .story-revealing .story-w { opacity: 1; transition: none; }

.story-portrait {
  position: relative;
  flex: 0 0 128px;
  width: 128px; height: 160px; /* 4:5, the portrait art's shape */
  border: 2px solid var(--accent);
  border-radius: 6px;
  background: radial-gradient(circle at 50% 35%, var(--story-race-color, #555), #111 110%);
  display: flex; align-items: center; justify-content: center;
  overflow: hidden;
}
.story-portrait img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
.story-initials {
  font-family: "Cinzel", Georgia, serif;
  font-size: 30px;
  font-weight: 700;
  color: rgba(255, 255, 255, 0.9);
  text-shadow: 0 2px 3px rgba(0, 0, 0, 0.6);
}
.story-portrait-small { flex-basis: 84px; width: 84px; height: 105px; } /* 4:5 like the dialogue portraits (enlarged 2026-09-26, user-directed) */
.story-portrait-small img { object-position: 50% 20%; }
.story-portrait-small .story-initials { font-size: 24px; }

/* Narration: a parchment-toned box, no portrait. */
.story-line-narration {
  min-height: 0;
  padding: 14px 18px;
  background: linear-gradient(180deg, rgba(233, 220, 192, 0.10), rgba(233, 220, 192, 0.05));
  border-left: 3px solid var(--accent);
  border-radius: 4px;
}
.story-line-narration .story-text { font-style: italic; color: #e8dcc0; }

/* The Marchstone: a dark stone panel with a thin gold crack. */
.game-dialog-story.story-stone {
  border: 2px solid #8a6d2a;
  border-image: none;
  background: linear-gradient(180deg, #16161a, #0c0c0e);
  box-shadow: 0 0 24px rgba(201, 168, 87, 0.18) inset;
}
.story-portrait-stone { background: #1b1b20; border-color: #8a6d2a; }
.story-portrait-stone svg { width: 64px; height: 84px; }
.story-portrait-stone img { z-index: 1; }
/* Skip: a small X in the modal's top-right corner. */
.story-close {
  position: absolute;
  top: 6px;
  right: 8px;
  z-index: 2;
  width: 28px;
  height: 28px;
  padding: 0;
  border: none;
  border-radius: 4px;
  background: transparent;
  color: var(--text-dim);
  font-size: 16px;
  line-height: 28px;
  cursor: pointer;
}
.story-close:hover, .story-close:focus-visible { color: var(--accent); background: rgba(255, 255, 255, 0.06); }

/* Knowledge > Characters (js/ui/knowledgebase.js renderCharacters). */
.kb-character-thumb {
  width: 28px;
  height: 28px;
  border-radius: 4px;
  object-fit: cover;
  object-position: 50% 18%;
  flex: 0 0 auto;
}
.kb-character-header { align-items: flex-start; }
.kb-character-portrait {
  width: 128px;
  height: 160px;
  object-fit: cover;
  border-radius: 6px;
  border: 2px solid var(--race-accent, var(--accent));
  flex: 0 0 auto;
}
.kb-character-portrait.race-human { --race-accent: #8e44ad; }
.kb-character-portrait.race-elf { --race-accent: #3f8f5c; }
.kb-character-portrait.race-dwarf { --race-accent: #9a7b56; }
.kb-character-portrait.race-orc { --race-accent: #7a2e2e; }
.kb-character-portrait.race-halfellow { --race-accent: #c9a857; }
.kb-character-quote { font-style: italic; opacity: 0.9; margin: 12px 0 4px; }
.kb-character-list { margin: 4px 0 8px; padding-left: 20px; }
.kb-character-list li { margin: 2px 0; }
@media (max-width: 600px) {
  .kb-character-portrait { width: 84px; height: 105px; }
}

/* Interface menu > Replay Story Scenes (js/ui/dialog.js "storyReplay"). */
.story-replay-list {
  list-style: none;
  margin: 8px 0 12px;
  padding: 0;
  max-height: min(60vh, 460px);
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  gap: 4px;
}
.story-replay-item {
  width: 100%;
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 12px;
  text-align: left;
}
.story-replay-turn { opacity: 0.65; font-size: 12px; white-space: nowrap; }
.story-header { padding-right: 32px; }
#story-back-btn:disabled { opacity: 0.4; cursor: default; }
.story-stone-body { fill: #2c2c33; stroke: #4a4a55; stroke-width: 1.5; }
.story-stone-crack { fill: none; stroke: var(--accent); stroke-width: 2; }
.story-stone-words {
  font-family: "Cinzel", Georgia, serif;
  font-variant: small-caps;
  letter-spacing: 3px;
  font-size: 22px !important;
  color: var(--accent) !important;
}

.story-actions { display: flex; align-items: center; justify-content: flex-end; gap: 8px; margin-top: 14px; }
.story-progress { margin-right: auto; font-size: 12px; color: var(--text-dim); }

/* Barks: small, self-dismissing corner cards. */
#story-bark-stack {
  position: fixed;
  left: 16px;
  bottom: 96px;
  z-index: 900;
  display: flex;
  flex-direction: column;
  gap: 8px;
  max-width: min(420px, calc(100vw - 32px));
  pointer-events: none;
}
.story-bark {
  pointer-events: auto;
  cursor: pointer;
  display: flex;
  gap: 10px;
  align-items: flex-start;
  background: var(--bg-panel);
  border-width: 10px;
  border-style: solid;
  border-image-slice: 20;
  border-image-repeat: round;
  padding: 8px 10px;
  opacity: 0;
  transition: opacity 0.35s ease;
  box-shadow: 0 4px 14px rgba(0, 0, 0, 0.5);
}
.story-bark.story-stone { border: 2px solid #8a6d2a; border-image: none; }
.story-bark.story-bark-in { opacity: 1; }
.story-bark-body { flex: 1; min-width: 0; }
.story-bark-caption { font-style: italic; font-size: 11px; color: var(--text-dim); margin-bottom: 3px; }
.story-bark-name {
  font-family: "Cinzel", Georgia, serif;
  font-size: 11px;
  font-weight: 700;
  text-transform: uppercase;
  color: var(--accent);
}
.story-bark-text { font-size: 13px; line-height: 1.4; color: var(--text-main); }
:root[data-motion-reduced="true"] .story-bark { transition: none; }

@media (max-width: 600px) {
  .story-line { gap: 10px; min-height: 0; }
  .story-portrait { flex-basis: 84px; width: 84px; height: 105px; }
  .story-portrait-small { flex-basis: 64px; width: 64px; height: 80px; }
  .story-initials { font-size: 20px; }
  .game-dialog-story .story-text { font-size: 15px; }
  #story-bark-stack { left: 8px; right: 8px; bottom: 120px; max-width: none; }
}
