* { box-sizing: border-box; }

/* ---------- Overall game dimensions ----------

   --game-height is the single source of truth for the tab-content area's
   height (everything inside `main`, above the tab bar). All tab panes
   read from it, and the per-region heights inside the Play tab are
   tuned to sum to it. Iterate on the overall app footprint by tweaking
   this one number — the modules respond.

   --game-width controls the panel's max width through `main`'s
   max-width below. Same idea: change here, everything follows. */
:root {
  /* ===== Design tokens =====

     Single source of truth for app-wide colors and typography. All CSS
     files reference these vars instead of hard-coding values — change
     here, everything follows. Specialty colors (button primary,
     danger red, category card backgrounds, etc.) keep their literal
     hex codes; only the broadly-used neutrals are tokenized.

     Font colors:
       --font-color-1 — primary text (the deep brown that reads as
         "black" on the cream palette). Used for body copy, headings,
         input text, ruler names.
       --font-color-2 — secondary / muted text. Used for labels like
         "Spouse:", "Children:", "LEGACY:" — anything that should sit
         quieter than primary content.
       --font-color-danger — error / destructive text (form errors,
         the "Delete profile" + "End the line" button labels).
       --font-color-positive — success text (settings-saved confirm
         line, future positive-affirmation feedback).
       --font-color-on-fill — text that sits on a colored pill fill
         (genetic / environmental trait pills, future flat pill buttons).
         Slightly cooler/darker than --font-color-1 to read cleanly
         against pastel backgrounds.

     Backgrounds:
       --background-color-1 — main panel cream (the `main` element,
         modal containers, the cream surfaces of choice buttons, etc.).
       --background-color-2 — body wash + meter strip (slightly darker
         cream behind the panel and in inset regions).
       --background-color-3 — pure white (card faces, input fields,
         trait pills, era cards).
       --background-color-danger — soft coral fill for the danger
         button variant (Delete profile, End the line). Distinct from
         --font-color-danger: text-danger is a darker brick red used
         for inline error copy; bg-danger is lighter and more
         saturated so it reads cleanly as a button surface with white
         text on top.

     Font families — three-tier typographic hierarchy, loaded from
     Google Fonts in index.html with display=swap so the loading
     screen shows Georgia until each webfont arrives.
       --font-family-1 — Metamorphous, a medieval display face. Used
         for HEADERS: h1/h2/h3, modal titles, the active ruler's name
         on the play screen, the headline number on the end-of-dynasty
         modal, NPC modal name, throne-ascension prose. Flourish
         typography; not for body copy.
       --font-family-2 — Cardo, a refined Renaissance serif. Used for
         SUB-HEADERS: uppercase labels like CROWN / COFFERS / YEAR /
         LEGACY, "Current Ruler:", "Spouse:", section titles, tab
         buttons, end-of-dynasty stat labels. Anything that sits
         quieter than a header but louder than body copy.
       --font-family-3 — Lora, a humanist serif. Used for BODY copy:
         card text, choice buttons, kin values, ages, trait pills,
         form inputs, paragraph prose, leaderboard rows, settings
         forms. This is the default inherited via `body`. */
  --font-color-1: #2a241e;
  --font-color-2: #8a7a5a;
  --font-color-danger: #8a3a2a;
  --font-color-positive: #4a6a3a;
  --font-color-on-fill: #2d2d2d;
  /* Ornate gold frame — the inset double-rule border drawn on modals
     (.modal-container::before) and the login screen (#auth-screen::before).
     Defined here at :root so both surfaces share one source of truth. */
  --gold: #c89b3c;
  --gold-soft: rgba(200, 155, 60, 0.55);
  --modal-frame-inset: 8px;
  /* Trait-pulse outline endpoints — the pulsing green ring drawn around
     a trait pill while the meter odometer rolls (see .pill.pulsing).
     Animates between these two; dark is the "on" beat, light the "off." */
  --trait-pulse-dark: #84cf90;
  --trait-pulse-light: #c8edcf;
  --background-color-1: #f4ede0;
  --background-color-2: #f4ede0;
  --background-color-3: #fff;
  --background-color-danger: #c97766;
  --font-family-1: 'Metamorphous', Georgia, 'Times New Roman', serif;
  --font-family-2: 'Cardo', Georgia, serif;
  --font-family-3: 'Lora', Georgia, serif;

  /* Responsive panel height. Grows with the viewport so taller phones
     (iPhone 16/17, ~852–874px) use their extra vertical space instead of
     padding it with dead margin, while shorter phones shrink to fit.
       • dynamic calc(100dvh − 7.75rem) — the overhead is body padding
                        (1rem) + main padding-block (2.25rem) +
                        --tab-bar-zone (4.5rem). 100dvh tracks the dynamic
                        viewport (excludes mobile browser chrome). This is
                        the height that makes the WHOLE panel exactly fill
                        the viewport, which matters now that the page
                        itself is scroll-locked (see html/body) — the
                        panel must always fit or it'd be clipped.
       • floor 30rem  — keeps the panel usable on very short windows.
       • cap   45rem  — taller viewports stop growing here so the panel
                        doesn't get unwieldy on tablets/desktop.
     The reclaimed height flows into the card area (the play-screen flex
     column gives slack to .card-stage). */
  --game-height: clamp(30rem, calc(100dvh - 7.75rem), 45rem);
  --game-width: 480px;
  /* Height of the persistent chrome row at the top of #tab-shell
     (holds the Help button on the right + the Language flag on the
     left). Tab-pane content heights and the play-screen card-stage
     subtract this so the overall panel footprint stays constant;
     --game-height represents the tab-shell content envelope (chrome
     row + tab pane), not the pane alone. */
  --chrome-row-height: 2rem;
  /* Source of truth for the bottom tab bar's full layout footprint.
     The .tab-bar rule sets `height: var(--tab-bar-zone)` and zeros its
     margin so this is the EXACT vertical space the bar occupies in
     #tab-shell — no phantom margins or padding outside the height.
     Loading + auth screens (which don't mount the shell) add it back
     via calc(--game-height + --tab-bar-zone) so the panel total
     height is identical across loading / auth / tab-shell. Tune in
     one place. */
  --tab-bar-zone: 4.5rem;
  /* main's inner padding lives in a var so fullscreen modals (which
     render outside of `main` and need to visually match it) can read
     the same value. Edit here, both follow.

     Side padding trimmed from 2rem → 1rem to give phone-portrait
     viewports (~390px) more horizontal room for card text, kin lines,
     and meter labels. Top/bottom unchanged. */
  --game-padding: 1.25rem 1rem 1rem;

  /* Shared left-edge x-coordinate for the Play tab's "value columns" —
     ruler name + age, ruler trait pills, spouse name, children list.
     All four sit at the same horizontal start so the eye can read them
     as one column of facts. Tune in one place. */
  --play-text-column-start: 4.5rem;

  /* ===== Theme colors — single source of truth =====
     Each card's narrative category (BASIC / SICKNESS / WAR / FAMILY /
     SCANDAL — extensible via tuning/themes.json) gets a background +
     border color. Defined ONLY here. The in-game .card-face rules
     below reference these variables; the editor + audit also read
     these values at server boot via game-server/utils/themeColors.js
     (which regex-parses this file) so the same numbers paint the
     editor's per-card theme dropdown and the audit's theme pills.
     Naming convention is strict: `--theme-<ID>-(bg|border|text)`
     where <ID> matches the theme id in tuning/themes.json. Add a new
     theme by adding its three variables here, a matching CSS rule for
     .card.theme-<ID> .card-face below, and an entry in themes.json
     (id + label + that's it — colors NOT in JSON). */
  --theme-BASIC-bg:    #f5e6b8;  /* parchment gold */
  --theme-BASIC-border: #b89860;
  --theme-BASIC-text:  #4a3a1a;
  --theme-SICKNESS-bg:    #d8e8d0;  /* pale herbal green */
  --theme-SICKNESS-border: #7a9468;
  --theme-SICKNESS-text:  #1a4a1a;
  --theme-WAR-bg:    #e8c8c8;  /* dusky red */
  --theme-WAR-border: #a85a5a;
  --theme-WAR-text:  #5a1a1a;
  --theme-FAMILY-bg:    #f5d8e0;  /* soft rose */
  --theme-FAMILY-border: #c08aa0;
  --theme-FAMILY-text:  #5a1a3a;
  --theme-SCANDAL-bg:    #e0d0ec;  /* lavender — whispers */
  --theme-SCANDAL-border: #8a6aa4;
  --theme-SCANDAL-text:  #3a1a4a;
}

/* App-style locked viewport. The page itself never scrolls or rubber-
   bands — the game is a fixed panel centered in the window, and only
   designated inner regions (modal bodies, the children-line, family
   tree, etc.) scroll via their own overflow. overscroll-behavior: none
   kills the elastic bounce/scroll-chaining on touch devices so the whole
   thing feels like a native app rather than a web page. */
html {
  height: 100%;
  overflow: hidden;
  overscroll-behavior: none;
}

body {
  margin: 0;
  /* Body copy default — Lora. Headers and sub-headers override below
     and in their own rules; `font-family: inherit` on form controls /
     buttons picks this up correctly. */
  font-family: var(--font-family-3);
  /* Damask backdrop framing the cream game panel. A dark gradient is
     layered over the image to deepen it slightly so the panel + its
     box-shadow still pop against the busy pattern. background-color
     (the old cream) stays as the fallback before the image loads /
     if it's missing. */
  background-color: var(--background-color-2);
  background-image:
    linear-gradient(rgba(40, 10, 14, 0.35), rgba(40, 10, 14, 0.35)),
    url('/assets/backgrounds/background01.png');
  background-repeat: no-repeat, repeat;
  background-size: cover, auto;
  background-position: center, center;
  background-attachment: fixed, fixed;
  color: var(--font-color-1);
  /* Lock the body to exactly the viewport height and hide overflow so the
     page can't scroll even when content is slightly tall. 100dvh tracks
     the *dynamic* viewport (excludes the mobile browser toolbars) to
     avoid the classic "100vh is too tall" jump; 100vh is the fallback. */
  height: 100vh;
  height: 100dvh;
  overflow: hidden;
  overscroll-behavior: none;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0.5rem;
}

main {
  /* Aged-parchment texture backing the whole gameplay container — shows
     through behind all four tabs (the tab shell + panes are transparent;
     only card faces / pills paint their own opaque surfaces on top).
     The cream background-color stays as the pre-load / fallback fill. */
  background-color: var(--background-color-1);
  background-image: url('/assets/backgrounds/background02.png');
  background-size: cover;
  background-position: center;
  border: 1px solid #c8b890;
  border-radius: 4px;
  padding: var(--game-padding);
  max-width: var(--game-width);
  width: 100%;
  box-shadow: 0 4px 20px rgba(70, 50, 30, 0.08);
  overflow: hidden;
}

/* Headers — Metamorphous (medieval display). Includes the document
   HTML headings + any non-heading element that wants header-weight
   typography (those get the font-family applied directly in their
   own rules; see .ruler-line, .end-legacy-number, .npc-title, etc.). */
h1, h2, h3, h4 {
  font-family: var(--font-family-1);
}

h1 {
  margin: 0 0 0.25rem;
  font-size: 2.5rem;
  font-weight: normal;
  text-align: center;
  letter-spacing: 0.05em;
}

h2 {
  margin: 0 0 0.5rem;
  font-size: 1.5rem;
  font-weight: normal;
  text-align: center;
}

/* ---------- Choose-an-Era / Name-your-House: fixed three-band stack ----------
   Same pattern as the login screen: key art (top quarter), title + subtitle
   (second quarter), interactive content (bottom half). The play-inner screen
   is a flex column filling its area (height set above), and the three bands
   take fixed 25 / 25 / 50 shares of it regardless of content. */

/* Key-art band — top quarter. Reserved empty in production (art is TBD);
   the FTUE bench drops a placeholder in to judge proportions. Mirrors
   .auth-keyart. */
.ftue-keyart {
  flex: 0 0 25%;
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
}
.ftue-keyart img {
  max-width: 100%;
  max-height: 100%;
  object-fit: contain;
}

/* Shared FTUE header — the title + subtitle block (second quarter), so both
   screens read identically. The h1 is the app title; .ftue-subtitle is the
   screen's prompt. Content centers within the band. */
.ftue-header {
  flex: 0 0 25%;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  text-align: center;
}

/* Interactive band — bottom half. Holds the era list or the house form.
   Scrolls within the band when its content is taller than the half (e.g.
   more eras than fit); otherwise its child's auto margins anchor the
   content low for thumb reach. */
.ftue-content {
  flex: 0 0 50%;
  display: flex;
  flex-direction: column;
  min-height: 0;
  overflow-y: auto;
}

/* Screen subtitle ("Choose an Era" / "Name your House") — a step up in
   size from the login subtitle, set in the sub-header serif and sitting
   directly under the title. A little top margin lifts it off the large
   display title above. */
.ftue-subtitle {
  margin: 0.4rem 0 0;
  font-family: var(--font-family-2);
  font-size: 1.15rem;
  color: var(--font-color-1);
  letter-spacing: 0.01em;
}

/* ---------- Loading screen ----------
   First thing the player sees on page load. Title + a slowly spinning
   hourglass so they know the app is alive while strings/content/whoami
   resolve in parallel. */
#loading-screen {
  text-align: center;
}

.loading-hourglass {
  display: block;
  margin: 1.5rem auto 0;
  width: 3.6rem;
  height: 3.6rem;
  object-fit: contain;
  /* Spins clockwise about its own centerpoint as the "working" cue. */
  transform-origin: center;
  animation: loading-spin 1.6s linear infinite;
}

.loading-message {
  margin: 0.75rem 0 0;
  color: #6a5a4a;
  font-style: italic;
}

/* Continuous clockwise rotation about the centerpoint — the shared
   "something is happening" cue for every hourglass icon (loading screen +
   card back). Linear so the spin is steady rather than easing each cycle. */
@keyframes loading-spin {
  from { transform: rotate(0deg); }
  to   { transform: rotate(360deg); }
}

/* ---------- Auth + dynasty creation forms ---------- */

.auth-tabs {
  display: flex;
  margin-bottom: 1.5rem;
  border-bottom: 1px solid #c8b890;
}

.auth-tab {
  flex: 1;
  background: none;
  border: none;
  padding: 0.5rem;
  font-family: var(--font-family-2);
  font-size: 0.95rem;
  cursor: pointer;
  color: var(--font-color-2);
  border-bottom: 2px solid transparent;
  margin-bottom: -1px;
}

.auth-tab.active {
  color: var(--font-color-1);
  border-bottom-color: #2a241e;
}

.auth-form, #found-dynasty-form {
  display: none;
  flex-direction: column;
  gap: 0.75rem;
}

.auth-form.active, #found-dynasty-form {
  display: flex;
}

.auth-form input, #found-dynasty-form input {
  padding: 0.6rem 0.75rem;
  font-family: inherit;
  font-size: 1rem;
  background: var(--background-color-3);
  border: 1px solid #c8b890;
  border-radius: 3px;
  color: inherit;
}

.auth-form input:focus, #found-dynasty-form input:focus {
  outline: none;
  border-color: #2a241e;
}

/* Invisible spacer input in the sign-in form — reserves the height (and
   the form's flex gap) of the sign-up Email field so the two forms are the
   same height and nothing moves when the tabs swap. visibility: hidden
   keeps the box in layout; display: none would collapse it and shift the
   Sign-in button / divider / Google button upward. */
.auth-spacer-field {
  visibility: hidden;
}

/* "House [____________]" — fixed prefix label + input field */
.house-name-input {
  display: flex;
  align-items: stretch;
}

.house-prefix {
  display: flex;
  align-items: center;
  padding: 0.6rem 0.85rem;
  background: var(--background-color-2);
  border: 1px solid #c8b890;
  border-right: none;
  border-radius: 3px 0 0 3px;
  color: #6a5a4a;
  font-style: italic;
  font-size: 1.2rem;   /* match the enlarged name input beside it */
}

/* ID-prefixed so this beats the base `#found-dynasty-form input` rule
   (which otherwise wins on specificity and forces 1rem + all-rounded
   corners). Squares the left corners to join the House prefix, and sizes
   the field up — it's the screen's primary input, and >16px also avoids
   the iOS zoom-on-focus jump. */
#found-dynasty-form .house-name-input input {
  flex: 1;
  border-radius: 0 3px 3px 0;
  font-size: 1.2rem;
}

/* Extra breathing room between the house field and the Begin button, on top
   of the form's base 0.75rem gap — so "Begin" reads as a separate commit
   step rather than crowding the field. */
#found-dynasty-form .button-primary {
  margin-top: 1rem;
}

/* Dice re-roll button — sits at the right edge of the house-name input,
   inside the same bordered row. Borderless and transparent so the dice
   emoji is the affordance, like the family-tree button on the play screen. */
.dice-trigger {
  background: none;
  border: none;
  padding: 0 0.6rem;
  font-size: 1.2rem;
  line-height: 1;
  cursor: pointer;
  color: inherit;
  font-family: inherit;
}

.dice-trigger:hover { opacity: 0.7; }
.dice-trigger:focus { outline: none; opacity: 0.7; }

/* Auth-screen + dynasty-creation submit buttons use the shared
   .button-primary class (see ui/buttons.css). No per-form button
   rules live here anymore. */

.error {
  margin: 0;
  color: var(--font-color-danger);
  font-size: 0.875rem;
  min-height: 1.2em;
}

.divider {
  display: flex;
  align-items: center;
  margin: 1.5rem 0;
  color: var(--font-color-2);
  font-size: 0.875rem;
}

.divider::before, .divider::after {
  content: '';
  flex: 1;
  height: 1px;
  background: #c8b890;
}

.divider span {
  padding: 0 1rem;
}

#google-button-container {
  display: flex;
  justify-content: center;
  min-height: 40px;
}

.hidden {
  display: none !important;
}

/* ---------- Stable wrapper footprint ----------
   Lock the height of every top-level screen to --game-height so the
   `main` container doesn't reflow as the player moves between
   loading → auth → tabs → play. Width is already constant via main's
   max-width.

   Auth + loading additionally vertically-center their content so the
   visible elements sit in the middle of the panel — there's enough
   vertical headroom that anchoring to the top would leave a large
   blank area below. */
#loading-screen,
#auth-screen {
  /* Match the mounted-tab-shell total so the panel doesn't visibly
     reflow when boot transitions from loading or auth into the shell.
     #tab-shell stacks chrome-row + tab-pane + tab-bar; loading/auth
     hide the shell entirely and need to substitute the tab-bar's
     footprint to keep main's height identical. See --tab-bar-zone. */
  height: calc(var(--game-height) + var(--tab-bar-zone));
  display: flex;
  flex-direction: column;
}

/* Ornate gold frame on the loading + login screens — same double-rule
   border the modals use, but pulled out close to the PANEL edge. Both
   screens fill <main>'s content box, so they sit inside main's padding
   (1.25rem top / 1rem sides+bottom); the frame's negative insets reach
   back OUT through that padding to ~6px from the panel edge. <main> is
   overflow: hidden so nothing spills past the edge. Extra inner padding
   pushes the content well inside the (now closer) frame. pointer-events:
   none keeps fields + buttons interactive underneath; anchored to these
   two screens so the frame shows only pre-game (loading + login), not on
   the in-shell tab screens. The inset calc references MAIN's padding, not
   the screen's own, so both screens frame identically. */
#loading-screen,
#auth-screen {
  position: relative;
  /* Roomier margins for the content, clearing the closer frame. */
  padding: 1.25rem 1.5rem;
}
#loading-screen::before,
#auth-screen::before {
  content: '';
  position: absolute;
  /* 6px from the panel edge on each side: 6px minus main's padding on
     that side (negative reaches outward toward the edge). */
  top: calc(6px - 1.25rem);
  right: calc(6px - 1rem);
  bottom: calc(6px - 1rem);
  left: calc(6px - 1rem);
  pointer-events: none;
  z-index: 1;
  border: 2px solid var(--gold);
  border-radius: 2px;
  box-shadow: inset 0 0 0 1px var(--background-color-1),
              inset 0 0 0 3px var(--gold-soft);
}

/* Inner play-tab sub-screens: subtract the chrome row's height from
   --game-height so the chrome row + sub-screen together still sum to
   --game-height. The chrome row only exists inside #tab-shell (which
   wraps the four tab panes), so #loading-screen and #auth-screen
   above don't need this adjustment. */
#era-selection,
#dynasty-creation,
#play-screen {
  height: calc(var(--game-height) - var(--chrome-row-height));
  display: flex;
  flex-direction: column;
}

/* Loading: stack the content and float it to the vertical center.
   (#auth-screen has its own fixed three-band layout below; the in-game
   inner screens — era-selection, dynasty-creation, play-screen — override
   this on their own.) */
#loading-screen {
  justify-content: center;
}

/* ---------- Login screen: fixed three-band stack ----------
   #auth-screen is a flex column filling the framed panel. The three bands
   take fixed shares of that height regardless of their content:
     • .auth-keyart  — top quarter, reserved for key art (TBD)
     • .auth-header  — second quarter, app title + subtitle
     • .auth-fields  — bottom half, the sign-up / sign-in fields
   flex: 0 0 <pct> locks each band to its share of the framed content box;
   25 + 25 + 50 = 100, so they exactly fill the screen. */
.auth-keyart {
  flex: 0 0 25%;
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
}
/* Placeholder/real art fills the reserved band without distorting. */
.auth-keyart img {
  max-width: 100%;
  max-height: 100%;
  object-fit: contain;
}
.auth-header {
  flex: 0 0 25%;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  text-align: center;
}
/* Login subtitle (the app tagline) — enlarged so it reads as a proper
   subtitle beneath the title within the second band. Keeps the muted
   italic character of the old tagline, just larger. */
.auth-subtitle {
  margin: 0.5rem 0 0;
  font-style: italic;
  color: var(--font-color-2);
  font-size: 1.05rem;
  line-height: 1.45;
}
.auth-fields {
  flex: 0 0 50%;
  display: flex;
  flex-direction: column;
  /* If the fields are taller than the band on a short screen, scroll
     within the band rather than spilling past it / breaking the 25/25/50
     proportions. On tall phones they fit without scrolling. */
  min-height: 0;
  overflow-y: auto;
}
/* Tighten the in-band spacing so the tabs + form + divider + Google
   button fit inside the bottom half on common phone heights (the band
   still scrolls on very short screens via overflow-y above). */
.auth-fields .auth-tabs { margin-bottom: 1rem; }
.auth-fields .divider { margin: 1rem 0; }

/* Dynasty-creation form — chosen-era reminder + house field + Begin, one
   grouped unit. It's the only child of the bottom-half .ftue-content band;
   margin-top: auto anchors it LOW within that band for thumb reach, and a
   small bottom margin keeps it off the band's bottom edge. */
#dynasty-creation #found-dynasty-form {
  margin-top: auto;
  margin-bottom: 1rem;
}

/* ---------- Play screen ----------

   The play screen is a vertical stack of fixed-height regions:
     house-name-row → dynasty-stats (Year / 🌳 / Legacy) → ruler-info →
     kin-block → meters → card-area
   Every region has a hard height; if content overruns, the region
   scrolls internally (horizontally for trait/kin pills, vertically for
   card text). The screen total sums to --game-height, so swapping
   between Play and any other tab is a clean swap with no reflow. */

.house-name {
  font-size: 1.85rem;
  font-weight: normal;
  letter-spacing: 0.04em;
  margin: 0;
}

.house-name-row {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.5rem;
  /* Match .tab-title exactly so the Play-tab header sits at the same
     vertical baseline as Profile / Leaders / Create. Previously
     2.75rem tall with a tight 0.15rem bottom margin — that put the
     centered text ~4px lower than the other tabs' headers. */
  height: 2.25rem;
  flex: 0 0 auto;
  margin-bottom: 0.4rem;
}

/* Year / 🌳 / Legacy — a 3-column grid so the tree button anchors
   the exact horizontal center regardless of the year + legacy text
   widths (a flex space-between would let the center drift as content
   changes). Year/Legacy keep their Cardo small-caps typography; the
   tree button keeps its borderless .ftree-trigger styling. Row height
   is set to fit the tree button rather than the text — that lets us
   retire the separate .tree-row above this strip and collapse two
   rows into one. */
.dynasty-stats {
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  /* Sub-header tier — Cardo. The Year / Legacy strip reads as UI
     chrome (uppercase small caps), not as body copy or a header. */
  font-family: var(--font-family-2);
  /* Same size as the Spouse/Children/Current-Ruler labels — every
     label-style line on the play screen reads at 1rem in font-color-2. */
  font-size: 1rem;
  color: var(--font-color-2);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  height: 2.2rem;
  flex: 0 0 auto;
  margin-bottom: 0.6rem;
}

.dynasty-stat-left { text-align: left; }
.dynasty-stat-right { text-align: right; }

/* Year digit span — wraps just the current_year number inside the
   "Year 1352" / "Year 1352 / 1450" composition so the odometer can
   roll those digits when the year advances between cards. Tabular-nums
   keeps each digit the same width as '0' so the rolled digit columns
   (1ch each) match the plain-text width pixel-for-pixel — same trick
   used on .meter-value. See app.js renderYear + render/meterOdometer.js. */
.year-digits {
  font-variant-numeric: tabular-nums;
  font-feature-settings: "tnum" 1;
}

/* Ruler info block: a fixed-height region with three pieces stacked:
     1. "Current Ruler:" label row on top (full-width, muted).
     2. Below: a 2-column grid — large icon on the left spanning 2 rows,
        then name+age (row 1) and traits (row 2) stacked on the right.
   Total height is fixed; trait overflow scrolls horizontally inside
   #traits-display so the block height stays exact. */
.ruler-info {
  height: 5.5rem;
  flex: 0 0 auto;
  margin-bottom: 0.6rem;
  display: flex;
  flex-direction: column;
  justify-content: center;
  text-align: center;
}

/* "Current Ruler:" label sits at the top of the block in the muted
   secondary tone — same color as the Spouse / Children labels below. */
.ruler-label-row {
  /* Sub-header tier — same Cardo family as Spouse: / Children: labels
     and the Year / Legacy strip so all "section" labels share weight. */
  font-family: var(--font-family-2);
  font-size: 1rem;
  color: var(--font-color-2);
  height: 1.4rem;
  line-height: 1.4rem;
  margin-bottom: 0.3rem;
}

/* Three-column grid:
     col 1 — large icon, spans 2 rows, anchored to the left.
     col 2 — name+age (row 1) + traits (row 2), left-aligned and butted
             up against the icon (no horizontal gap).
     col 3 — family-tree button, spans 2 rows, anchored to the right
             edge of the row.
   The icon column is sized to ~2× the previous .ruler-icon emoji
   width so the active ruler reads as the focal point. */
.ruler-info-grid {
  display: grid;
  grid-template-columns: 3.6rem 1fr;
  grid-template-rows: auto auto;
  column-gap: 0;
  row-gap: 0.2rem;
  align-items: center;
  /* Override the parent's text-align: center for the icon + text row —
     text in col 2 should sit left-justified against the icon, not
     centered across the row width. The tree button used to occupy a
     third column here; it now lives in the center of the Year / 🌳 /
     Legacy strip above this block. */
  text-align: left;
}

.ruler-info-grid > .ruler-icon {
  grid-column: 1;
  grid-row: 1 / span 2;
  /* width + height matched so the cell stays square; the emoji /
     iconography-png inside centers via flex. The font-size drives the
     emoji glyph size — bumped from 2.8rem so the active ruler reads
     even louder as the focal point of the play screen. */
  width: 3.6rem;
  height: 3.6rem;
  font-size: 3.4rem;
  line-height: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  margin: 0;          /* override the legacy .ruler-icon margin */
  vertical-align: baseline;
}

.ruler-line {
  grid-column: 2;
  grid-row: 1;
  /* Bumped from 1rem so the ruler name+age reads larger than the
     surrounding labels. Line-height + height grow proportionally. */
  font-size: 1.25rem;
  color: #4a3e30;
  height: 1.7rem;
  line-height: 1.7rem;
  text-align: left;
  /* Anchor the ruler name's left edge to the shared play-text-column
     x-coordinate. The icon column is 3.6rem wide; everything after
     that is padding. Defined this way so changing --play-text-column-start
     moves the ruler text + traits + kin values together. */
  padding-left: calc(var(--play-text-column-start) - 3.6rem);
}
/* The ruler's name is the screen's headline; render it in the
   header face (Metamorphous). The age that follows ("· 42") stays in
   the inherited body font so the data piece reads as data, not as
   display copy. */
.ruler-line #ruler-name {
  font-family: var(--font-family-1);
}

/* Family-tree button — emoji-only affordance, no chrome. Same
   borderless-transparent pattern as .dice-trigger / .help-trigger.
   Used in the centered slot of the play-screen .dynasty-stats grid
   and on the end-of-dynasty modal (.end-family-tree owns its own
   positioning). */
.ftree-trigger {
  background: none;
  border: none;
  padding: 0.25rem 0.5rem;
  font-size: 1.6rem;
  line-height: 1;
  cursor: pointer;
  color: inherit;
  font-family: inherit;
}
.ftree-trigger:hover { opacity: 0.7; }
.ftree-trigger:focus { outline: none; opacity: 0.7; }

/* Bitmap icon mounted inside the family-tree triggers — used wherever
   the player can open the tree (play header, end-of-dynasty, past
   dynasties, leaderboard). Sized to match the surrounding text scale
   so the trigger button reads at the same visual weight whether it's
   showing the icon (current) or the emoji (legacy). Distinct class
   name from the FamilyTree modal's own `.ftree-icon` (which wraps
   each person's crown emoji) so the two rules don't collide. */
.ftree-trigger-icon {
  width: 1.6rem;
  height: 1.6rem;
  object-fit: contain;
  vertical-align: middle;
  display: inline-block;
}

.dot {
  color: #c8b890;
  margin: 0 0.1rem;
}

/* Kin block holds spouse + children as a 2-column grid:
     col 1: "Spouse:" / "Children:" labels — fixed-width so col 2
            starts at the shared --play-text-column-start anchor,
            matching the ruler name + traits' x-coordinate above.
     col 2: the value cells — spouse name (single row) or the
            comma-separated children list (wraps to multi row
            inside its own cell).
   Block height is fixed; if the brood overflows, the children value
   cell scrolls vertically inside its row. */
.kin-block {
  height: 5rem;
  flex: 0 0 auto;
  margin-bottom: 0.6rem;
  display: grid;
  grid-template-columns: var(--play-text-column-start) 1fr;
  grid-template-rows: auto 1fr;
  column-gap: 0;
  row-gap: 0.3rem;
  align-items: start;
}

.kin-label {
  /* Sub-header tier — Cardo, matches Current Ruler: / YEAR / LEGACY. */
  font-family: var(--font-family-2);
  color: var(--font-color-2);
  font-size: 1rem;
  line-height: 1.4rem;
  text-align: left;
  white-space: nowrap;
  /* Small inside-cell gutter so the label text doesn't kiss the value
     column's start. The value column's left edge is fixed at
     --play-text-column-start; this padding is purely visual breathing
     room within col 1. */
  padding-right: 0.5rem;
}

.kin-value {
  /* Sub-header tier — same Cardo family + size as .kin-label so the
     label/value pair reads as one balanced metadata row (Lora at 1rem
     has a wider x-height than Cardo at 1rem, which made the names read
     louder than their labels). Mirrors the all-Cardo .dynasty-stats
     strip above. */
  font-family: var(--font-family-2);
  color: var(--font-color-2);
  font-size: 1rem;
  line-height: 1.4rem;
  text-align: left;
  min-width: 0;       /* let long content shrink + wrap inside */
}

/* ---------- Pill-shaped tags — shared across the app
   (trait chips on the play screen and NPC modal; later: dynasty traits,
   relationship badges, status indicators, etc.) */

.pill-list {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem;
  justify-content: center;
}

/* Inside the play screen's fixed-height ruler-info, trait pills must
   stay on one row. Many-trait rulers get a horizontal scroll inside
   #traits-display instead of wrapping (which would blow the region's
   reserved height). Other .pill-list usages (NPC modal, etc.) keep
   wrapping — they're inside scrollable modal bodies.
   Lives in the ruler-info grid's row 2, column 2 — left-aligned
   against the icon column so the pills sit flush with the name above. */
#traits-display {
  grid-column: 2;
  grid-row: 2;
  flex-wrap: nowrap;
  overflow-x: auto;
  /* overflow-y must be `visible` so the pulsing 4px trait-outline ring
     isn't clipped at the top/bottom of the row. A horizontal scroll
     container normally forces overflow-y to compute as `auto` (clipping),
     so we instead reserve room with vertical padding equal to the ring
     width and pull it back out with an equal negative margin — the
     layout doesn't move, but the ring now has paint space above/below.
     The negative margins are NOT applied left/right so the scroll edges
     still behave. */
  overflow-y: visible;
  margin-top: -5px;
  margin-bottom: -5px;
  justify-content: flex-start;
  scrollbar-width: none;     /* Firefox: hide scrollbar; row stays single-height */
  /* Same calc as .ruler-line so traits pills start at the shared
     play-text-column x-coordinate. */
  padding-left: calc(var(--play-text-column-start) - 3.6rem);
  /* 5px top/bottom = ring width (4px) + 1px breathing room. Combined
     with the equal negative margins above, the visible spacing is
     unchanged from the original padding-bottom: 0.15rem. */
  padding-top: 5px;
  padding-bottom: calc(0.15rem + 5px);
}

#traits-display::-webkit-scrollbar { display: none; }

.pill {
  padding: 0.15rem 0.6rem;
  border: 1px solid #c8b890;
  border-radius: 999px;
  font-size: 0.85rem;
  background: var(--background-color-3);
  color: var(--font-color-1);
  white-space: nowrap;
  font-family: inherit;
  flex: 0 0 auto;
}

/* Trait pill variants — driven by tuning/traits.json's `kinds` map.
   GENETIC traits (born with — Sickly/Robust/Beautiful/Plain) get a
   saturated rose fill that reads as "constitution / body." ENVIRONMENTAL
   traits (shaped by the ruler's reign — Pious/Ruthless/etc.) get a saturated
   sage fill that reads as "personality / upbringing." Flat — no border —
   so the fill itself does the work of distinguishing the two kinds at a
   glance. Unclassified traits fall back to the neutral base .pill style. */
.pill.pill-genetic {
  background: #e9cbcd;
  border: none;
  color: var(--font-color-on-fill);
}
.pill.pill-environmental {
  background: #c5d8ba;
  border: none;
  color: var(--font-color-on-fill);
}

/* Trait pulse — fires on a trait pill while the meter odometer rolls,
   when that trait is multiplying one of the affected meters (see
   app.js pulseTraitPills). A green outline that pulses exactly twice
   and does NOT change the pill's size.

   The cycle is anchored at the LIGHT value (frame 0 + frame 100), peaking
   at dark in the middle — so the ring swells UP from light to dark on
   first appearance (not snapping on dark from nothing), and because each
   cycle both starts AND ends on light, two iterations begin and end on
   the off-state — the pulse fades out gracefully on its own at a keyframe
   boundary rather than being cut mid-swell. Two 850ms cycles ≈ the
   1500ms odometer, close enough that they read as aligned without the JS
   trying to clip the animation to an exact deadline.

   The outline is drawn with box-shadow (a 4px ring) rather than the CSS
   `outline`/`border` properties so it doesn't shift layout or fight the
   pill's existing 1px border — it floats just outside the pill edge and
   follows the 999px pill radius. */
@keyframes trait-pulse-outline {
  0%, 100% { box-shadow: 0 0 0 4px var(--trait-pulse-light); }
  50%      { box-shadow: 0 0 0 4px var(--trait-pulse-dark); }
}
.pill.pulsing {
  animation-name: trait-pulse-outline;
  animation-duration: 850ms;
  animation-timing-function: ease-in-out;
  animation-iteration-count: 2;
  /* Hold the final keyframe (light, i.e. effectively invisible against
     the pill) so there's no flash back to a default state when the two
     cycles finish before the class is cleaned up. */
  animation-fill-mode: forwards;
  /* Lift the pulsing pill above its neighbors so the 4px ring renders on
     top of adjacent pills instead of behind them. Paired with the
     pill-list's overflow: visible (below) so the ring isn't clipped by
     the row's bounds either. */
  position: relative;
  z-index: 1;
}

/* ---------- Play screen ruler details ----------
   The main .ruler-icon cell is styled in the .ruler-info-grid > .ruler-icon
   rule above. Below: PNG-variant sizing shared with other icon contexts
   (kin lines, family tree). */

/* Phase 2 PNG portrait — drops in alongside emoji wherever the
   IconographyRenderer decides art exists. Sized in `em` so it scales
   with the surrounding text without needing per-context overrides.
   No circular clip or fill — the PNGs float as-is, so their transparent
   backgrounds show whatever surface they sit on. object-fit: contain so
   the whole portrait is visible (cover would crop it to the box). */
.iconography-png {
  display: inline-block;
  width: 1.4em;
  height: 1.4em;
  vertical-align: -0.3em;
  object-fit: contain;
}

/* The main ruler icon gets a slightly larger PNG so the play screen
   has visual weight on the active reign. The icon cell's font-size
   is now 2.8rem (see .ruler-info-grid > .ruler-icon), so 1em ≈ that
   size — keep the PNG close to cell-filling. */
.ruler-icon .iconography-png {
  width: 1em;
  height: 1em;
  vertical-align: middle;
}

/* ---------- Kin grid: value-cell overrides ----------
   Base typography (color / font-size / line-height) is in .kin-value
   above. Per-cell specializations live here:
     • #spouse-line  — one row max, never wraps; the ruler has at most
                       one spouse, and a single nowrap line is fine.
     • #children-line — wraps inside its grid cell; scrolls vertically
                       when the brood exceeds the cell's allotted height. */

#spouse-line {
  height: 1.4rem;
  white-space: nowrap;
  overflow: hidden;
}

#children-line {
  height: 100%;
  overflow-y: auto;
  scrollbar-width: thin;
  padding-right: 0.25rem;   /* gutter so wrapped text doesn't kiss the scrollbar */
}

#children-line::-webkit-scrollbar {
  width: 4px;
}
#children-line::-webkit-scrollbar-thumb {
  background: #c8b890;
  border-radius: 2px;
}

.meters {
  display: grid;
  /* 5 equal columns — CROWN, COFFERS, FAITH, POPULARITY, GIFT — spanning
     the full card width. No horizontal padding, so column 1's left edge
     sits on the card's left edge and column 5's right edge on the card's
     right edge. (The card fills the stage width on phone-portrait, so the
     meters row and the card share edges.) The GIFT cell's chevrons are
     pinned to its own left/right edges, so the right chevron lands on the
     card's right edge. */
  grid-template-columns: repeat(5, 1fr);
  gap: 0.4rem;
  padding: 0.4rem 0;
  /* No solid backing — the four meter readouts + the gift icon float
     directly over the main game background. */
  height: 3rem;
  flex: 0 0 auto;
  /* margin-top: auto absorbs any leftover space between the kin block
     and meters when --game-height grows. Card stays pinned to its
     fixed height; the gap above meters takes up the slack. */
  margin-top: auto;
  margin-bottom: 0.6rem;
}

.meter {
  display: flex;
  flex-direction: column;
  align-items: center;
  font-size: 0.75rem;
  /* Positioning context for the upkeep toast — see .meter-upkeep-toast
     below. The toast is absolutely positioned below the meter value
     during the post-resolve family-upkeep animation. */
  position: relative;
}

/* Upkeep toast — a floating "👶 −2" annotation that fades in below a
   meter's value during the family-upkeep deduction phase of a card
   resolve. The JS appends/removes this element; CSS owns positioning +
   the fade-in/out transitions. */
.meter-upkeep-toast {
  position: absolute;
  bottom: 100%;
  left: 50%;
  transform: translate(-50%, -4px);
  white-space: nowrap;
  font-size: 0.7rem;
  color: var(--meter-down-color, #b03a2a);
  opacity: 0;
  pointer-events: none;
  transition: opacity 200ms ease-out, transform 200ms ease-out;
}
.meter-upkeep-toast.visible {
  opacity: 1;
  transform: translate(-50%, -8px);
}

/* Bonus variant — positive/green float for gains (trait synergy
   "2x Traits"). Overrides the default deduction red. Slightly bolder
   so the "this helped you" beat reads clearly against the meter row. */
.meter-upkeep-toast.bonus {
  color: var(--font-color-positive);
  font-weight: 700;
}

/* Action-card variant of the toast: a small icon flying up off the
   meter cell. Sized roughly to match the family-upkeep "👶" emoji
   footprint so the two toasts read as siblings — same beat, different
   cause. */
.meter-upkeep-toast img {
  width: 1.1rem;
  height: 1.1rem;
  object-fit: contain;
  display: block;
  filter: drop-shadow(0 1px 1px rgba(70, 50, 30, 0.25));
}

.meter-label {
  /* Sub-header tier — uppercase chrome label (CROWN, COFFERS, FAITH,
     POPULARITY). Cardo. */
  font-family: var(--font-family-2);
  color: var(--font-color-2);
  text-transform: uppercase;
  letter-spacing: 0.05em;
  font-size: 0.65rem;
  margin-bottom: 0.05rem;
}

.meter-value {
  font-size: 1.4rem;   /* enlarged now the row reclaimed its side padding */
  font-weight: normal;
  line-height: 1.1;
  /* Tabular figures make every digit occupy the same width as '0'.
     Combined with the 1ch-wide digit columns used during the odometer
     roll, this guarantees the cell's total width is identical whether
     the value is rendered as plain text or as rolling digit strips —
     so the end-of-roll handoff (or the post-response renderMeters
     rebuild) doesn't visibly shift the number's horizontal position. */
  font-variant-numeric: tabular-nums;
  font-feature-settings: "tnum" 1;
}

/* Odometer roll state — applied for the animation window during which
   a numeric value is rolling from its old value to its new one. The
   element becomes a row of digit columns; each column hides overflow
   and a .digit-strip inside translates vertically to reveal the
   current digit. See render/meterOdometer.js.

   Column width is 1ch (the width of the '0' glyph). Combined with
   font-variant-numeric: tabular-nums on the parent element, this
   guarantees a rolled "53" is the same width as a plain "53" so the
   end-of-animation handoff doesn't shift the layout.

   Used by:
     • .meter-value (Crown / Coffers / Faith / Popularity numbers)
     • .year-digits (the YEAR display in the dynasty header)
   Both add .odometer to themselves to opt in. */
.odometer.rolling {
  display: inline-flex;
  vertical-align: baseline;
  line-height: 1.1;
}
.odometer.rolling .digit-col {
  display: inline-block;
  width: 1ch;
  height: 1.1em;
  overflow: hidden;
  text-align: center;
  position: relative;
}
.odometer.rolling .digit-col.sign-col {
  width: 0.5ch;
}
.odometer.rolling .digit-strip {
  display: flex;
  flex-direction: column;
  line-height: 1.1;
  will-change: transform;
}
.odometer.rolling .digit-strip > span {
  display: block;
  height: 1.1em;
  text-align: center;
}

/* Direction tinting — applied to the same element as .rolling. Color
   cascades down into the digit strips. The .rolling-up class lasts for
   the full odometer window; at the end the JS swaps it for .pulsing,
   which animates color from the rolling tint back to the default brown
   and briefly scales the cell up so the recovery reads as a flash, not
   a fade. The starting color is passed in as the --pulse-from custom
   property by the JS so a single keyframe rule serves both directions. */
:root {
  --meter-up-color: #4a8a3a;     /* green — gains */
  --meter-down-color: #b03a2a;   /* red — losses */
}
.odometer.rolling-up   { color: var(--meter-up-color); }
.odometer.rolling-down { color: var(--meter-down-color); }
.odometer.pulsing {
  animation: meter-pulse 500ms ease-out forwards;
}
@keyframes meter-pulse {
  0%   { color: var(--pulse-from, var(--font-color-1)); transform: scale(1.15); }
  100% { color: var(--font-color-1);                    transform: scale(1); }
}

/* Card region. Text scrolls inside the upper area so the two choice
   buttons stay pinned at the bottom — they never get pushed off-screen
   by long text, and the meters/kin lines above never shift.

   Fixed height so the card stays a consistent size regardless of
   --game-height. Any extra vertical space in the panel is absorbed by
   margin-top: auto on .meters, which pushes the kin/ruler/header
   regions toward the top and lets the meters+card pair sit at the
   bottom of the panel. */
/* ---------- VFX overlay ----------

   Transient visual-effect surface, mounted over <main> by the VFX
   dispatcher (render/vfx/index.js) and removed automatically when
   the effect finishes. CSS here only owns positioning + click pass-
   through; per-petal animation is JS-driven via rAF + inline
   transforms (see render/vfx/flowerShower.js for the model). */
.vfx-overlay {
  position: absolute;
  inset: 0;
  pointer-events: none;
  overflow: hidden;
  z-index: 50;            /* above card-stage, below modal backdrops (z-100) */
}
.vfx-petal {
  position: absolute;
  top: 0;
  left: 0;
  line-height: 1;
  user-select: none;
  /* will-change: transform hints the compositor that this node will
     animate; pairs with the inline translate/rotate updates per
     frame to keep paint cost low. */
  will-change: transform;
}

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

   The stage is a positioning context for one (or briefly two) .card
   elements as they slide in / out. Each card is `position: absolute`
   filling the stage; the stage owns the fixed height so the panel
   below doesn't reflow when a card is mid-flight.

   The stage intentionally does NOT clip horizontally — cards translate
   past the panel's left/right padding and visibly slide through the
   body wash beyond the panel border, then off the viewport entirely
   on mobile. Vertical layout still respects the stage's fixed height
   (cards never escape vertically because their motion is purely
   horizontal).

   Animation choreography (see app.js):
     1. Click choice → outgoing card transitions to translateX(120%)
        (slides out to the right, fully past the panel edge), ~350ms.
     2. ~100ms later, a new face-down card mounts at translateX(-120%)
        (off the left, in the body wash) and transitions to
        translateX(0) — slight overlap with the outgoing slide so the
        cards visibly cross.
     3. When BOTH the slide-in animation has finished AND the next
        card's text+choices payload is in memory, the new card flips
        (rotateY 180deg, 500ms) to reveal its face. The flip is the
        ONLY moment the player sees the text/choices appear — no
        post-flip lag. */

.card-stage {
  position: relative;
  /* No overflow clipping — see block comment above. Cards slide past
     the panel border into the body wash. */
  /* GROW to absorb the play-screen's slack. --game-height is now
     responsive (taller on iPhone 16/17), and the reclaimed vertical
     space flows HERE rather than into dead margin above the meters — so
     the 4:3 card scales up on taller phones. The min-height keeps the
     stage at its original size on the shortest phones (it used to be a
     fixed calc(17.75rem − chrome row)); flex-grow takes any extra. */
  flex: 1 1 auto;
  min-height: calc(17.75rem - var(--chrome-row-height));
  /* Size container so the card-inner can compute its 4:3 box against the
     stage's actual height/width via cqh/cqw units — the precise way to
     fit the largest non-cropping 4:3 card whether the stage is limited by
     its height (wide phones) or its width (narrow phones). */
  container-type: size;
  /* Transparent so the panel's own background-color-1 cream shows
     through. When a card slides in/out, the negative space reads as
     part of the larger play panel rather than a darker "table" inset.
     The cards themselves carry the visible color (face cream / back
     pattern). */
  background: transparent;
  border-radius: 3px;
  /* 3D context for child flip transforms. */
  perspective: 1200px;
  /* Breathing room between the card's bottom edge and the tab-bar
     footer below — without this the card kisses the divider line
     and looks cramped. Bumped from 0.5rem after collapsing the old
     tree-row + Year/Legacy strip into a single row: half the saved
     vertical lands here as an explicit margin, the other half flows
     into the gap above .meters via its margin-top: auto absorbing
     the remaining slack from the play-screen flex column. */
  margin-bottom: 1rem;
}

/* A single card. Lives inside .card-stage. The transform-controlled
   transitions on .card animate horizontal entry/exit; the flip lives
   one level deeper on .card-inner so the slide and the flip compose
   without conflicting. */
.card {
  position: absolute;
  inset: 0;
  background: transparent;       /* the face / back own their fill */
  /* Slide easing — fast curve out (departing card needs no emphasis)
     and a smoother curve in. */
  transition: transform 350ms cubic-bezier(0.5, 0, 0.75, 0);
  will-change: transform;
}

/* The 3D-flipping pair. preserve-3d lets the two children stack on
   either side of the same plane and rotate together.

   LOCKED 4:3 — the flip container (not the two faces separately) carries
   the card's shape, so the FRONT and BACK are guaranteed identical size
   (no more wide-front / 4:3-back mismatch on flip). Centered in the stage
   via the absolute-centering idiom (inset: 0 + margin: auto + explicit
   size) so the flip's rotateY transform stays free of any centering
   translate. Width drives: 4:3 against the (now taller, responsive) stage
   height; max-height clamps to the stage; max-width keeps it inside
   narrow panels — so the card scales up on tall phones and shrinks whole
   on small ones, never cropping. */
.card-inner {
  position: absolute;
  inset: 0;
  margin: auto;          /* center the fixed-size box within the stage */
  /* Largest 4:3 box that fits the stage: width is the smaller of the
     stage's width (100cqw) and its height-derived width (100cqh × 4/3).
     Height follows from the 4:3 aspect-ratio. So the card fills the stage
     height on wide phones and the stage width on narrow ones — always
     4:3, never cropped, front and back identical. */
  width: min(100cqw, calc(100cqh * 4 / 3));
  aspect-ratio: 4 / 3;
  transform-style: preserve-3d;
  transition: transform 500ms cubic-bezier(0.4, 0, 0.2, 1);
}

/* Both faces fill the card. backface-visibility: hidden lets the
   front hide the back (and vice versa) during a flip.

   inset: 6px (instead of 0) leaves a thin gutter inside .card for
   the box-shadow to breathe. (Historically this gutter was load-
   bearing because .card-stage clipped via overflow: hidden; the
   stage no longer clips, but the gutter remains for visual
   consistency.) The shadow lives ON the face/back (not on the outer
   .card) so it follows the flip plane: only the front-facing
   surface renders its shadow at any given moment, while the back-
   facing one is hidden by backface-visibility. The result reads as
   a card lifted off the panel with a shadow that tracks both the
   slide and the flip. */
.card-face,
.card-back {
  position: absolute;
  inset: 6px;
  /* Slightly rounded corners — reads as a playing-card edge rather
     than the sharper panel/input radius elsewhere in the UI. */
  border-radius: 10px;
  backface-visibility: hidden;
  -webkit-backface-visibility: hidden;
  /* Stronger, layered drop shadow so the card reads as a physical object
     lifted off the table: a tight, darker contact shadow just under the
     edge, plus a larger softer ambient shadow for depth. Lives on the
     face/back (not the outer .card) so it tracks the flip plane. */
  box-shadow:
    0 2px 4px rgba(50, 35, 20, 0.30),
    0 10px 22px rgba(50, 35, 20, 0.38);
}

/* Card back: the visible side at rest (when .card-inner is at 0deg).
   Heraldic illustration (cardBacking01.png).

   LOCKED ASPECT RATIO (4:3 landscape). The back is the art surface, so it
   gets a fixed shape that never crops the artwork — author cardBacking
   art at 4:3 (e.g. 800×600). It's centered within the card; the diagonal-
   stripe gradient remains as a fallback if the PNG is missing.

   Why 4:3: the stage is a FIXED height (~240px for the back) with a fluid
   width that caps ~436px. Deriving width from the fixed height at 4:3
   gives ~320px — comfortably inside common phones (390px-class) and snug
   on small ones (360px). max-width: 100% lets it shrink-to-fit (whole,
   ratio preserved) on anything narrower so it never overflows; on the
   common sizes it renders at the clean fixed 4:3.

   Overriding the base .card-face/.card-back `inset: 6px` fill: the back
   is now a centered aspect box instead of a full-bleed rectangle. The
   .card-face (front) keeps the full-fill behavior — its scrolling text +
   pinned choice buttons depend on filling the stage. */
.card-back {
  /* Fills .card-inner (which owns the locked 4:3 shape), so the back IS
     4:3 and matches the front exactly through the flip. Heraldic
     illustration; the diagonal-stripe gradient stays as a fallback if the
     PNG is missing. Author cardBacking art at 4:3 (e.g. 800×600). */
  background-image:
    url('/assets/backgrounds/cardBacking01.png'),
    repeating-linear-gradient(
      45deg,
      var(--background-color-1) 0,
      var(--background-color-1) 12px,
      #ead9b6 12px,
      #ead9b6 24px
    );
  background-size: cover, auto;
  background-position: center, center;
  background-repeat: no-repeat, repeat;
  border: 1px solid #b89860;
}

/* Card front: hidden behind the back at rest (rotated 180deg so it
   lives on the far side of the flip plane). When .card-inner rotates
   180deg via the .face-up class, the front comes around to face the
   viewer. Same cream-surface look the old .card had — padded inner
   column with text on top and choice buttons pinned at the bottom.
   The source/category color rules below target .card-face so the
   visual treatment lands on the visible side once flipped. */
.card-face {
  background: var(--background-color-3);
  border: 1px solid #c8b890;
  padding: 1.25rem;
  display: flex;
  flex-direction: column;
  transform: rotateY(180deg);
}

/* Center the back's "card back" placeholder cue. A small inset border
   gives the back a finished feel even at the diagonal-stripe stage. */
.card-back::after {
  content: '';
  position: absolute;
  inset: 0.6rem;
  border: 1px solid rgba(184, 152, 96, 0.6);
  /* Tracks the outer card-back's 10px radius — concentric curves
     read as a finished frame. */
  border-radius: 6px;
  pointer-events: none;
}

/* Hourglass centered on the card back. The card sits face-down during
   the brief window the server is fetching the next card's content, so
   the spinning hourglass tells the player "something is happening" — the
   same vocabulary as the loading-screen hourglass (same glyph, same
   loading-spin animation). Sits above the diagonal-stripe pattern but
   below the ::after inset border in stacking order (which is fine; the
   border is just a thin outline near the edge). */
.card-back::before {
  content: '';
  position: absolute;
  inset: 0;
  /* Hourglass PNG centered on the card back as the loading cue. Drawn as
     a centered background-image (rather than content: url) so it can be
     sized; same clockwise spin as the loading-screen hourglass. The
     pseudo-element fills the card (inset: 0), so rotating it about its
     center spins the centered glyph on its own centerpoint. */
  background-image: url('/assets/ui/icon_Hourglass.png');
  background-repeat: no-repeat;
  background-position: center;
  background-size: 3.6rem 3.6rem;
  transform-origin: center;
  animation: loading-spin 1.6s linear infinite;
  pointer-events: none;
}

/* ---------- State classes (set by JS) ----------
     .entering   — mounted off-screen left, immediately transitions to neutral.
     .exiting    — at neutral, transitions to translateX(120%) then unmounts.
     .face-up    — .card-inner is rotated 180deg → back hidden, face shown.
                   (Without this class, the inner sits at 0deg = back shown
                   on the front side via rotateY(180deg) above; with it,
                   the whole thing flips and the front becomes visible.)
   The "face-down at rest" state is just: card mounted, no .face-up.
   The flip happens by toggling .face-up on. */

.card.entering {
  /* Starting position: off the left edge of the stage. The slide
     happens by removing this class after one paint, which lets the
     element transition to its default (translateX(0)) position. */
  transform: translateX(-120%);
}

.card.exiting {
  transform: translateX(120%);
}

.card.face-up .card-inner {
  transform: rotateY(180deg);
}

/* Source-of-card markers. Scaffolded so future sources (crisis-era,
   dynasty-trait unlock, etc.) can each get their own visual treatment by
   adding a class + a rule here. The selectors target .card-face so the
   source/category color lands on the visible side of the flipper. */
.card.queue .card-face {
  background: #f5e6b8;        /* light parchment gold */
  border-color: #b89860;      /* warm darker border */
  border-width: 2px;
}

/* .card.random — intentionally has no extra .card-face styling; the
   default cream surface above IS the random-source treatment. Add a
   rule here if/when a distinct look is wanted. */

/* Theme-of-card markers. Independent of functional source (Pool / Queued
   / Triggered) — these describe what KIND of event the card represents.
   Set in the card JSON via `"theme": "BASIC" | "SICKNESS" | "WAR" |
   "FAMILY" | "SCANDAL"` (extensible — themes are registered in
   tuning/themes.json with id + label; colors come from the
   `--theme-<ID>-*` custom properties in :root above). Theme rules
   come AFTER source rules so a queued FAMILY card reads visually as
   FAMILY rather than as "queued." Colors are the design tokens at
   the top of this file — edit them once there to change the palette
   everywhere (game, editor, audit). */
.card.theme-BASIC .card-face {
  background: var(--theme-BASIC-bg);
  border-color: var(--theme-BASIC-border);
  border-width: 2px;
}

.card.theme-SICKNESS .card-face {
  background: var(--theme-SICKNESS-bg);
  border-color: var(--theme-SICKNESS-border);
  border-width: 2px;
}

.card.theme-WAR .card-face {
  background: var(--theme-WAR-bg);
  border-color: var(--theme-WAR-border);
  border-width: 2px;
}

.card.theme-FAMILY .card-face {
  background: var(--theme-FAMILY-bg);
  border-color: var(--theme-FAMILY-border);
  border-width: 2px;
}

.card.theme-SCANDAL .card-face {
  background: var(--theme-SCANDAL-bg);
  border-color: var(--theme-SCANDAL-border);
  border-width: 2px;
}

/* Card is mid-resolve — the optimistic-UI preview has applied locally
   (meters updated) and we're waiting for the server response with the
   next card. This class is now mostly historical (the slide-out anim
   handles the visual change), but kept as a no-op hook in case a
   future state needs to disable choice buttons during transition. */
.card.resolving {
  pointer-events: none;
}

.card-text {
  margin: 1px 0 1rem;   /* +1px top to clear the staged-gift row */
  font-size: 1.05rem;
  line-height: 1.5;
  flex: 1 1 auto;
  min-height: 0;       /* lets the flex child actually shrink + scroll */
  overflow-y: auto;
  padding-right: 0.25rem;
}

.card-choices {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  flex: 0 0 auto;
  /* Inset the stacked choice pills 10px on each side so they sit a touch
     narrower than the full card width. The buttons are width:100% of this
     padded box (see .card-choices .button-primary below). */
  padding: 0 10px;
}

/* Choice buttons inside a card now use the shared .button-primary
   pill — same dark-fill / cream-text vocabulary as the main CTAs
   elsewhere in the app. Stretching to the full card-choices column
   gives the two stacked pills a consistent edge alignment instead
   of shrinking to their text width. The .unaffordable class on the
   button (set by app.js when a choice's coffers cost would go
   negative) is paired with `disabled` on the element, so the shared
   :disabled rule in buttons.css handles the opacity/cursor styling
   — no per-class override needed here. */
.card-choices .button-primary {
  width: 100%;
}

/* Developer-only tuning overlay on choice buttons. Visible only when
   the active player's is_developer flag is true — see
   auth/isDeveloper.js + render/devTuningOverlay.js.

   Two affordances:
     • .tuning-net — small signed integer after the choice text showing
       the net meter impact (after trait modifiers). Green / red / gray.
     • .tuning-tooltip — full breakdown, revealed on button hover.
       Light background, slightly bigger font so it pops against the
       dark choice button. */

/* Choice buttons get position:relative so the absolutely-positioned
   tooltip anchors to them. The .card-choices .button-primary rule
   above already gives them width:100%. */
.card-choices .button-primary {
  position: relative;
}

.tuning-net {
  margin-left: 0.4em;
  font-weight: bold;
  font-size: 0.85em;
}
.tuning-net.pos  { color: #9adb95; }
.tuning-net.neg  { color: #e89090; }
.tuning-net.zero { color: #c8b890; }

.tuning-tooltip {
  /* Hidden by default; revealed on button hover via the selector
     below. Pointer-events disabled so the tooltip itself never
     intercepts the click — the underlying button always gets it. */
  display: none;
  position: absolute;
  bottom: calc(100% + 8px);
  left: 50%;
  transform: translateX(-50%);
  z-index: 200;

  background: #fefdf9;
  color: #2a241e;
  border: 1px solid #c8b890;
  border-radius: 4px;
  box-shadow: 0 6px 18px rgba(40, 30, 20, 0.25);
  padding: 0.6rem 0.8rem;
  min-width: 280px;
  max-width: 380px;
  font-size: 0.95rem;
  font-family: Georgia, 'Times New Roman', serif;
  font-weight: normal;
  font-style: normal;
  text-align: left;
  line-height: 1.4;
  letter-spacing: 0;
  pointer-events: none;
  white-space: normal;
}
.card-choices .button-primary:hover .tuning-tooltip {
  display: block;
}

.tuning-tooltip .tt-table {
  border-collapse: collapse;
  width: 100%;
  margin-bottom: 0.4rem;
}
.tuning-tooltip .tt-table td {
  padding: 0.15rem 0.4rem 0.15rem 0;
  vertical-align: baseline;
  font-size: 0.9rem;
}
.tuning-tooltip .tt-meter {
  color: #6a5a4a;
  font-size: 0.8rem;
  text-transform: uppercase;
  letter-spacing: 0.05em;
}
/* Tighten the columns around the arrow so per-trait pills in the
   trait cell have more horizontal room and don't wrap on narrow
   tooltips. Base/final are right-aligned numerics; the arrow column
   is narrow; the trait cell gets its own padding (rule below). */
.tuning-tooltip .tt-base,
.tuning-tooltip .tt-final {
  font-family: 'Courier New', monospace;
  font-weight: bold;
  text-align: right;
  min-width: 1.8em;
  padding-right: 0.1rem;
  padding-left: 0.1rem;
}
.tuning-tooltip .tt-arrow {
  color: #999;
  text-align: center;
  width: 0.7em;
  padding: 0.15rem 0;
}
.tuning-tooltip .tt-arrow-blank {
  /* Reserves the column footprint when no traits contributed, so the
     rows still align cleanly down the table. */
  visibility: hidden;
}
.tuning-tooltip .pos  { color: #2a7a3a; }
.tuning-tooltip .neg  { color: #a83232; }
.tuning-tooltip .zero { color: #999; }
.tuning-tooltip .tt-trait {
  display: inline-block;
  font-style: italic;
  font-size: 0.78rem;
  padding: 0 0.3em;
  margin-right: 0.2em;
  border-radius: 3px;
}
.tuning-tooltip .tt-trait.pos { color: #2a6a3a; background: #e0eed8; }
.tuning-tooltip .tt-trait.neg { color: #8a2a2a; background: #f0d8d0; }
.tuning-tooltip .tt-cap {
  font-size: 0.72rem;
  color: #6a5a4a;
  font-style: italic;
}
.tuning-tooltip .tt-trait-cell {
  padding-left: 0.4rem;
}
.tuning-tooltip .tt-net {
  margin-top: 0.3rem;
  padding-top: 0.3rem;
  border-top: 1px solid #e8dfc8;
  font-weight: bold;
  text-align: right;
}
.tuning-tooltip .tt-env,
.tuning-tooltip .tt-other {
  margin-top: 0.35rem;
  font-size: 0.85rem;
  color: #4a3a2a;
}
.tuning-tooltip .tt-trait-bias {
  display: inline-block;
  background: #efe7c8;
  color: #4a3a1a;
  border-radius: 3px;
  padding: 1px 6px;
  margin-right: 3px;
  font-size: 0.8rem;
  font-style: italic;
}
.tuning-tooltip .tt-trait-bias.tt-dead {
  background: #e8e0d8;
  color: #8a7a6a;
  text-decoration: line-through;
}
.tuning-tooltip .tt-label {
  color: #6a5a4a;
  font-size: 0.75rem;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  margin-right: 0.3em;
}

/* Death-screen layout moved to ui/modals/DeathModal.css — the death event
   now overlays the play screen as a modal so the main container stays
   mounted (more "app-like" feel). The .death-actions class previously
   lived here; it's been moved alongside the modal-specific pieces. */

/* ---------- Action buttons (logout, delete, continue) ---------- */

/* ---------- Tab shell ----------

   Persistent 4-tab UI: Play, Profile, Leaders, Create. Only one tab pane
   visible at a time. The tab bar sits at the bottom of <main> and stays
   visible whenever the user is logged in (loading + auth screens hide
   the entire shell via #tab-shell.hidden).

   Modals overlay the tab bar via .modal-backdrop (z-index: 100 in
   modals.css). The bar visually persists underneath the backdrop's gray
   wash but is unclickable while a modal is open. Fullscreen modals
   cover the bar entirely. */

#tab-shell {
  display: flex;
  flex-direction: column;
}

/* ===== Chrome row =====
   Thin persistent row at the top of #tab-shell. Holds the corner
   affordances (Help on the right; Language flag, reserved-but-empty,
   on the left) on their own real row so the tab headings below them
   never overlap these buttons. Fixed height — the card-stage's height
   is reduced by the same amount so --game-height stays unchanged.

   Sized to the help button: 1.2rem font + 0.25rem padding ≈ 1.7rem
   tall; 2rem gives a small breathing margin. */
.chrome-row {
  display: flex;
  justify-content: space-between;
  align-items: center;
  height: var(--chrome-row-height);
  flex: 0 0 auto;
}

/* Reserved-width slots so the right-button position is stable
   regardless of whether the left slot is empty (today) or holds the
   future Language flag. Width matches the help button's effective
   footprint (1.2rem glyph + 0.5rem horizontal padding × 2 ≈ 2.2rem)
   so dropping the flag in later won't shift the layout. */
.chrome-slot {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 2.2rem;
  height: 1.7rem;
}

/* Persistent corner affordances in the chrome row — Help on the right,
   Language flag on the left. Both use the same borderless transparent
   "emoji-as-affordance" treatment as .dice-trigger / .ftree-trigger.
   Hidden automatically when #tab-shell.hidden is set. */
.help-trigger,
.lang-trigger {
  background: none;
  border: none;
  padding: 0.25rem 0.5rem;
  font-size: 1.2rem;
  line-height: 1;
  cursor: pointer;
  color: var(--font-color-1);
  font-family: inherit;
}
.help-trigger:hover, .lang-trigger:hover { opacity: 0.7; }
.help-trigger:focus, .lang-trigger:focus { outline: none; opacity: 0.7; }

/* Bitmap help icon inside the chrome-row Help button (replaces the old
   ℹ️ emoji). Sized to match the emoji's footprint; the button keeps its
   borderless-transparent treatment. */
.help-trigger-icon {
  display: block;
  width: 1.35rem;
  height: 1.35rem;
  object-fit: contain;
}

/* Non-Play tabs match the post-chrome-row content envelope so the
   panel footprint stays identical when switching tabs. The chrome
   row above already eats --chrome-row-height; the tab content fills
   the rest of --game-height. The Play tab doesn't get this rule —
   its inner .play-inner divs own the height directly, and stacking
   the height on both parent and child would double up. */
#profile-tab,
#leaders-tab,
#badges-tab {
  height: calc(var(--game-height) - var(--chrome-row-height));
  display: flex;
  flex-direction: column;
}

/* The profile body can grow taller than --game-height (icon picker +
   form fields + buttons). Scroll inside the tab rather than letting
   the panel push the tab bar down. */
#profile-body {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  padding-right: 0.25rem;   /* keep content clear of the scrollbar gutter */
}

/* Section header inside each non-Play tab. Sized + positioned to
   match .house-name on the Play tab so all four tab headings sit in
   the same visual slot at the top of the panel. */
.tab-title {
  margin: 0 0 0.4rem;
  font-size: 1.5rem;
  font-weight: normal;
  text-align: center;
  letter-spacing: 0.04em;
  height: 2.25rem;
  line-height: 2.25rem;
  flex: 0 0 auto;
}

/* Global Profile-tab message row. Sits between the tab title and the
   tab body. Reserves a single fixed row regardless of content — so
   showing/hiding success or error text never reflows the form below.
   The body layout doesn't have to budget for inline message slots
   anymore; everything below this row stacks at its natural spacing. */
.profile-message {
  margin: 0 0 0.5rem;
  height: 1.3rem;
  line-height: 1.3rem;
  font-size: 0.875rem;
  text-align: center;
  color: var(--font-color-2);
  flex: 0 0 auto;
}
.profile-message.error { color: var(--font-color-danger); }
.profile-message.success { color: var(--font-color-positive); }

.tab-placeholder {
  text-align: center;
  padding: 2rem 0.5rem;
  color: #6a5a4a;
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  display: flex;
  align-items: center;
  justify-content: center;
}

.tab-placeholder-text {
  margin: 0;
  font-style: italic;
  line-height: 1.5;
}

/* Tab bar: four equal-width buttons at the bottom of the panel. The
   border-top mirrors the old .app-footer separator so the seam between
   panel content and tab bar reads consistently.
   The bar's layout footprint is locked to --tab-bar-zone — explicit
   height with margin: 0 means the variable IS the bar's full
   contribution, no phantom margins. The former 1rem margin-top is
   absorbed into padding-top so the visual breathing between tab
   content and the buttons stays similar. */
.tab-bar {
  display: flex;
  height: var(--tab-bar-zone);
  margin: 0;
  padding-top: 1.75rem;
  border-top: 1px solid #e6dcc4;
  gap: 0;
}

.tab-button {
  flex: 1;
  background: none;
  border: none;
  padding: 0.55rem 0.4rem;
  /* Sub-header tier — Cardo. Tab labels read as UI chrome, not body
     copy. */
  font-family: var(--font-family-2);
  font-size: 1.125rem;   /* +2px over the prior 1rem */
  color: var(--font-color-2);
  cursor: pointer;
  letter-spacing: 0.04em;
  border-top: 2px solid transparent;
  margin-top: -0.75rem;     /* overlap the bar's border-top so .active can paint over it */
  padding-top: calc(0.55rem + 0.75rem);
}

.tab-button:hover {
  color: var(--font-color-1);
}

.tab-button.active {
  color: var(--font-color-1);
  border-top-color: #2a241e;
}

/* ---------- Sub-tab bar (Leaders > Global / City / School) ----------
   A second-level tab strip inside a tab. Visually quieter than the
   main tab bar: thinner row, bottom-border indicator instead of top. */
.subtab-bar {
  display: flex;
  gap: 0;
  margin: 0 0 0.75rem;
  border-bottom: 1px solid #c8b890;
}

.subtab-button {
  flex: 1;
  background: none;
  border: none;
  padding: 0.45rem 0.4rem;
  /* Sub-header tier — Cardo. Subtab nav chrome (Global / City /
     School on the Leaders tab). */
  font-family: var(--font-family-2);
  font-size: 0.85rem;
  color: var(--font-color-2);
  cursor: pointer;
  border-bottom: 2px solid transparent;
  margin-bottom: -1px;
}

.subtab-button:hover { color: var(--font-color-1); }
.subtab-button.active {
  color: var(--font-color-1);
  border-bottom-color: #2a241e;
}

/* ---------- Leaders body — pinned scope header + scrollable list ----------

   Two-slot flex column. The header slot (.leaders-header) is built
   once and stays mounted across data swaps so a "City: Belmont" or
   "School: Stanford" label can pin under the sub-tabs while the list
   below scrolls. The scroll slot (.leaders-scroll) takes the remaining
   height and is the only thing replaced when the data loads or the
   scope changes. */
.leaders-body {
  flex: 1 1 auto;
  min-height: 0;
  display: flex;
  flex-direction: column;
}

.leaders-header {
  flex: 0 0 auto;
  padding: 0.4rem 0 0.6rem;
  border-bottom: 1px solid #c8b890;
  margin-bottom: 0.6rem;
  text-align: center;
}

.leaders-header:empty {
  display: none;     /* Global scope has no header — collapse the slot */
}

.leaders-scope-label {
  margin: 0;
  font-size: 0.95rem;
  color: var(--font-color-1);
}

.leaders-scope-label .leaders-scope-kind {
  color: var(--font-color-2);
  margin-right: 0.35rem;
}

.leaders-scroll {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  padding-right: 0.25rem;
}

.leaderboard-list {
  list-style: none;
  padding: 0;
  margin: 0;
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  counter-reset: leaderboard-rank;
}

/* Each row is a 5-column CSS grid:
     ┌──────┬──────┬─────────────────────┬─────────┬──────┐
     │      │      │  House Aldenwick    │ LEGACY  │      │
     │  1   │  👑  │─────────────────────│─────────│  🌳  │
     │      │      │  @user · Era        │   88    │      │
     └──────┴──────┴─────────────────────┴─────────┴──────┘
   Cols 1 (rank), 2 (icon), 5 (family-tree) each span both rows.
   Cols 3 (text) and 4 (legacy) split into a label-row + value-row.
   Middle column gets `min-width: 0` so long house names truncate
   instead of forcing the row to grow horizontally. Reuses the
   cream-surface visual vocabulary from Past Dynasties. */
.leaderboard-row {
  display: grid;
  /* Tighter rank and icon tracks so the row's left edge reads as one
     compact unit. The visible space between rank ↔ icon and icon ↔
     house text is mostly each cell's internal centering whitespace,
     so shrinking the tracks moves the needle far more than tweaking
     column-gap alone (which I tried first and was too subtle). */
  grid-template-columns: 1.8rem 2.2rem 1fr auto 2.8rem;
  grid-template-rows: auto auto;
  align-items: center;
  column-gap: 0;
  row-gap: 0.05rem;
  padding: 0.5rem 0.6rem;
  background: var(--background-color-3);
  border: 1px solid #c8b890;
  border-radius: 3px;
}

/* Owner-row highlight — applied when the archived dynasty's user_id
   matches the active player. Warm gold fill + matching border picks
   the player's own entries out of a dense list. The "YOU" marker
   (rendered by leaderboard.js in place of "@username") gets a darker
   readable tone against the gold; see .leaderboard-author-you below. */
.leaderboard-row.leaderboard-row-mine {
  background: #f4e0b0;
  border-color: #c89146;
}
.leaderboard-author-you {
  color: #5a3a1a;
  font-weight: 600;
  letter-spacing: 0.04em;
}

/* Rank — leftmost column, spans both rows. Big numeric column to anchor
   the eye to the player's position. Reads as a numbered list without
   needing a counter-reset CSS hack. */
.leaderboard-rank {
  grid-column: 1;
  grid-row: 1 / span 2;
  text-align: center;
  font-size: 1.7rem;
  font-weight: 600;
  color: var(--font-color-2);
  line-height: 1;
}

/* Icon cell — second column, spans both rows. Carries the player's
   profile emoji (or 👤 fallback). The rank badge that used to overlay
   this cell now lives in column 1 as its own block. */
.leaderboard-icon {
  grid-column: 2;
  grid-row: 1 / span 2;
  /* `width: 100%` so the icon cell fills the track (2.2rem) rather
     than its old fixed 2.8rem — that mismatch is what made tightening
     column-gap invisible last round. Height stays at 2.8rem so the
     emoji has room to breathe vertically. */
  width: 100%;
  height: 2.8rem;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 1.75rem;
  line-height: 1;
}

.leaderboard-house {
  grid-column: 3;
  grid-row: 1;
  min-width: 0;
  font-size: 1rem;
  color: var(--font-color-1);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.leaderboard-meta {
  grid-column: 3;
  grid-row: 2;
  min-width: 0;
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  font-size: 0.8rem;
  color: var(--font-color-2);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.leaderboard-author { color: var(--font-color-2); }
.leaderboard-era    { font-style: italic; }

/* Legacy column — small label stacked above a big value. The wrapper
   spans BOTH grid rows so its vertical center matches the rank cell's
   center (which also spans both rows). If label + value were placed
   into rows 1 and 2 separately, each child would center inside its
   own row, and the value would sit visibly below the rank's midline.
   `auto` width on column 4 lets it size to widest content (3-digit
   legacy `100` doesn't crowd, 2-digit `88` stays tight). */
.leaderboard-legacy {
  grid-column: 4;
  grid-row: 1 / span 2;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  /* Left-side gap is tight (column-gap: 0); the Legacy column reads
     as a standalone unit, so reclaim a few pixels back from itself. */
  margin-left: 0.4rem;
}

.leaderboard-legacy-label {
  /* Sub-header tier — uppercase "LEGACY" stub above each row's score. */
  font-family: var(--font-family-2);
  font-size: 0.65rem;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--font-color-2);
  line-height: 1;
}

.leaderboard-legacy-value {
  font-size: 1.25rem;
  font-weight: 600;
  color: var(--font-color-1);
  line-height: 1;
  /* Small gap below the label, no other space — the wrapper's
     justify-content: center handles the overall vertical placement. */
  margin-top: 0.15rem;
}

/* Family-tree button — rightmost cell, also spans both rows so it's
   vertically balanced against the icon + rank cells. Sized to the
   same 2.8rem square footprint as the icon cell; the existing
   .ftree-trigger naked-emoji styling is preserved. */
.leaderboard-row .leaderboard-ftree {
  grid-column: 5;
  grid-row: 1 / span 2;
  width: 2.8rem;
  height: 2.8rem;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 1.6rem;
  /* Tighten the legacy column → tree button gap is fine, but the
     button still wants a hair of separation from the legacy value
     to its left. */
  margin-left: 0.3rem;
}

/* Empty-state / gating prompt — when the player hasn't set home_city
   or home_school. Same surface as a placeholder, centered. */
.leaderboard-prompt {
  padding: 2rem 0.5rem;
  text-align: center;
  color: var(--font-color-2);
  font-style: italic;
  line-height: 1.5;
}

/* ---------- Geoapify autocomplete widget ----------
   Used by Profile for Home City / Home School. The input looks like
   any other settings-field text input; the dropdown floats below it. */

.geoapify-autocomplete {
  position: relative;
}

.geoapify-input {
  width: 100%;
  padding: 0.5rem 0.6rem;
  font-family: inherit;
  font-size: 0.95rem;
  background: var(--background-color-3);
  border: 1px solid #c8b890;
  border-radius: 3px;
  color: inherit;
}

.geoapify-input:focus {
  outline: none;
  border-color: #2a241e;
}

.geoapify-list {
  position: absolute;
  top: 100%;
  left: 0;
  right: 0;
  z-index: 50;
  margin: 0.2rem 0 0;
  padding: 0;
  list-style: none;
  background: var(--background-color-3);
  border: 1px solid #c8b890;
  border-radius: 3px;
  max-height: 12rem;
  overflow-y: auto;
  box-shadow: 0 4px 12px rgba(70, 50, 30, 0.15);
}

.geoapify-item {
  padding: 0.45rem 0.6rem;
  font-size: 0.9rem;
  cursor: pointer;
  border-bottom: 1px solid #e6dcc4;
}
.geoapify-item:last-child { border-bottom: none; }
.geoapify-item:hover { background: var(--background-color-2); }

/* ---------- Profile settings hint line ----------
   Italic, font-color-2. Sits below the autocomplete inputs to
   explain why the field exists ("Used for the City leaderboard"). */
.settings-hint {
  margin: 0.25rem 0 0;
  font-size: 0.8rem;
  font-style: italic;
  color: var(--font-color-2);
}

.game-actions {
  display: flex;
  gap: 0.5rem;
  justify-content: center;
  align-items: center;
}

/* Text-label button styles (primary / secondary / danger) live in
   ui/buttons.css. The .action-btn class is retired in favor of
   .button-primary / .button-secondary / .button-danger applied
   directly at the call sites. */

/* ---------- Era selection ----------

   The era cards live in the bottom-half .ftue-content band (below the key
   art + title/subtitle). margin-top: auto anchors the list LOW within that
   band for thumb reach when the cards fit; when there are more eras than
   fit, the auto margin collapses to 0 and the .ftue-content band scrolls.
   A small bottom margin keeps the lowest card off the band's edge. Same
   bottom-anchoring trick as #dynasty-creation #found-dynasty-form above. */

#era-list {
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
  margin: auto 0 1rem;
  width: 100%;
}

.era-card {
  background: var(--background-color-3);
  border: 1px solid #c8b890;
  border-radius: 4px;
  padding: 1rem;
  cursor: pointer;
  text-align: left;
  font-family: inherit;
  color: inherit;
}

.era-card:hover {
  background: var(--background-color-2);
  border-color: #2a241e;
}

.era-card-name {
  /* Header tier — the era's display name is the card's headline. */
  font-family: var(--font-family-1);
  font-size: 1.15rem;
  margin-bottom: 0.2rem;
}

.era-card-years {
  font-size: 0.85rem;
  color: var(--font-color-2);
  font-style: italic;
  margin-bottom: 0.5rem;
}

.era-card-desc {
  font-size: 0.9rem;
  color: #4a3e30;
  line-height: 1.4;
}

/* Chosen-era reminder ("Late Medieval Europe · 1300–1500"). Now the first
   child of #found-dynasty-form, so the form's own 0.75rem gap handles the
   spacing down to the house field — no negative top margin needed. */
.era-summary {
  text-align: center;
  font-style: italic;
  color: var(--font-color-2);
  font-size: 1.05rem;
  margin: 0;
}

/* ---------- End-of-dynasty modal ---------- */

/* The modal body is rendered by EndOfDynastyModal.js inside a
   .modal-fullscreen overlay. The .end-modal class is the body wrapper;
   everything centers inside it for the closing-chapter feel. */
.end-modal {
  text-align: center;
  /* Fill the scrollable modal body so the bottom section can pin to the
     floor. .modal-body is a flex column (see modals.css); growing here
     lets .end-bottom's margin-top:auto push it down. */
  display: flex;
  flex-direction: column;
  flex: 1 1 auto;
  min-height: 100%;
}

/* "THE LINE" heading + ruler roll + action buttons — bottom-justified.
   margin-top:auto absorbs the slack between the top cluster (tagline /
   context / legacy) and this block, pinning it to the bottom of the modal
   on taller viewports; on short content it simply sits below as before. */
.end-bottom {
  margin-top: auto;
}

/* "Current context" slot between the tagline and the legacy score.
   runLegacyReveal fills it in step with the odometer: first the reign-
   contribution line, then the badge tally (the line is SWAPPED for the
   tally here — unlike the death modal, which accumulates both). FIXED
   height reserves room for the taller tally up front, so the score below
   never shifts when the content swaps. Content is vertically centered
   within the slot (justify-content: center) since only one element shows
   at a time — that reads more balanced than top-aligning a lone line.

   The 8.5rem height matches the death modal's .death-context. Everything
   above the legacy is now fixed-height (title, fixed-height .end-tagline,
   this slot), so the Legacy tally lands at the SAME on-screen position as
   the death modal's floating tally — pinned regardless of the conditional
   tagline above or the badges that swap in. .end-bottom (THE LINE +
   actions) takes the only auto margin, pinning to the floor. */
.end-context {
  height: 8.5rem;
  margin: 0 0 0.25rem;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
}
.end-context-line {
  margin: 0;
  font-family: var(--font-family-2);
  font-size: 0.95rem;
  color: var(--font-color-1);
  opacity: 0;
  transform: translateY(4px);
  transition: opacity 280ms ease-out, transform 280ms ease-out;
}
.end-context-line.visible {
  opacity: 1;
  transform: translateY(0);
}

/* Conditional flavor line under the title ("…endured from…" / "…fell
   extinct…" / player-ended). FIXED height (reserves up to 3 wrapped lines)
   with content centered, so a 2-line vs 3-line tagline — or a longer dynasty
   name — never shifts the Legacy tally below it. The top margin is tuned so
   the Legacy lands at the SAME on-screen height as the death modal's floating
   tally, which is the reference position we pin every end screen to. */
.end-tagline {
  height: 4.5rem;
  margin: 0.9rem 0 1.5rem;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  font-style: italic;
  color: #4a3e30;
  line-height: 1.5;
}

/* Legacy headline — the dynasty's final score, rendered as the visual
   focal point of the end-of-dynasty modal. A big number on its own row
   with a small label below; a "🏆 New personal best!" badge appears
   above when summary.is_personal_best is set. The comparator line
   ("Previous best: 234" or "Your first dynasty.") sits below the label
   in muted type so the eye lands on the number first. */
/* Legacy tally floats over the modal's parchment — no solid backing —
   matching the death modal's .death-legacy treatment so the two
   end-of-dynasty surfaces read identically. */
.end-legacy {
  margin: 0 auto 1.5rem;
  padding: 0.5rem 1.25rem;
  text-align: center;
}
.end-legacy-number {
  /* Header tier — the headline score on the end-of-dynasty modal.
     Metamorphous as a flourish display moment. */
  font-family: var(--font-family-1);
  font-size: 3.25rem;
  line-height: 1;
  font-variant-numeric: tabular-nums;
  font-feature-settings: "tnum" 1;
  color: var(--font-color-1);
}
.end-legacy.end-legacy-new-best .end-legacy-number {
  color: #c89146;   /* warm gold — signals personal best */
}
.end-legacy-label {
  /* Sub-header tier — the uppercase "LEGACY" caption under the number.
     Bumped top margin from 0.15rem to 0.4rem so the label has visible
     breathing room from the headline number above. */
  font-family: var(--font-family-2);
  margin-top: 0.4rem;
  font-size: 0.7rem;
  color: var(--font-color-2);
  text-transform: uppercase;
  letter-spacing: 0.08em;
}
/* "🏆 New personal best!" callout — plain text line, not a pill.
   Earlier this was a rounded gold pill which read as a button.
   Sits between the LEGACY label and the previous-best comparator
   so the eye reads: number → LEGACY → "new personal best!" → previous. */
.end-legacy-new-best-line {
  margin-top: 0.5rem;
  font-size: 0.95rem;
  color: #c89146;        /* warm gold — matches the number tint */
  letter-spacing: 0.02em;
}
.end-legacy-comparator {
  margin-top: 0.3rem;
  font-size: 0.85rem;
  color: var(--font-color-2);
  font-style: italic;
}

/* X / Y legacy row on the end modal during the animated reveal — the big
   score (X) plus a smaller muted ceiling (Y), matching the death modal's
   X/Y treatment. The ceiling span is only rendered in the animated path. */
.end-legacy-score-row {
  display: flex;
  align-items: baseline;
  justify-content: center;
  gap: 0.15rem;
}
.end-legacy-cap {
  font-family: var(--font-family-1);
  font-size: 1.5rem;
  line-height: 1;
  color: var(--font-color-2);
  font-variant-numeric: tabular-nums;
  font-feature-settings: "tnum" 1;
}

/* The personal-best glow + comparator line. Hidden during the animated
   Legacy reveal; runLegacyReveal adds .visible only after the true final
   score is on screen, so the verdict lands last. The static path renders
   it with .visible already set. */
.end-legacy-reveal {
  opacity: 0;
  transition: opacity 320ms ease-out;
}
.end-legacy-reveal.visible {
  opacity: 1;
}

/* Two-button action row at the bottom of the end modal. Primary
   action is "Start a new dynasty"; secondary is "See Leaderboard".
   Buttons sit side-by-side and split the row — full-width buttons that
   stretch across the panel read as bombastic and waste horizontal
   space. Two buttons sharing a row is the project's default for
   paired actions. */
.end-actions {
  display: flex;
  flex-direction: row;
  gap: 0.5rem;
  margin-top: 1rem;
  justify-content: center;
}
.end-actions .button-primary,
.end-actions .button-secondary,
.end-actions .button-danger {
  flex: 1 1 0;
  max-width: 50%;
}

/* Family-tree affordance on the end-of-dynasty modal. Borderless,
   transparent — matches the play-screen tree button so the icon-only
   look reads as a consistent doorway into the tree across screens. */
/* "THE LINE" heading + its inline Family Tree button. The button sits
   just to the right of the heading, vertically centred, as a quiet
   doorway into the dynasty's tree right where the ruler roll begins. */
.end-line-header {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.45rem;
  margin: 0 0 0.5rem;
}
.end-line-header .end-section-title {
  margin: 0;
}
.end-family-tree {
  display: inline-flex;
  align-items: center;
  margin: 0;
  background: none;
  border: none;
  padding: 0.1rem;
  line-height: 1;
  cursor: pointer;
  font-family: inherit;
  color: inherit;
}
.end-family-tree:hover { opacity: 0.7; }
.end-family-tree:focus { outline: none; opacity: 0.7; }

.end-section-title {
  /* Sub-header tier — the "The line" uppercase divider above the
     ruler roll. h3 by default; the heading-rule above sets it to
     family-1, so we re-anchor it to family-2 here since visually it's
     small-caps chrome, not a headline. */
  font-family: var(--font-family-2);
  margin: 0 0 0.5rem;
  font-size: 1rem;
  font-weight: normal;
  color: var(--font-color-2);
  text-transform: uppercase;
  letter-spacing: 0.05em;
  text-align: center;
}

.ruler-roll {
  list-style: decimal inside;
  padding: 0;
  margin: 0 0 1.5rem;
  text-align: left;
  font-size: 0.9rem;
  line-height: 1.5;
  color: #4a3e30;
}

.ruler-roll li {
  margin-bottom: 0.25rem;
}

/* ===== Action Card / gift carousel =====
   The 5th meter-row cell. When the ruler holds one or more gifts in
   their inventory, .meter-action becomes the .gift-carousel (see the
   gift-carousel rules below) — a spinnable icon the player taps to stage
   a gift onto the live card face (.card-action-cards). When the
   inventory is empty (or every held gift is already staged) the cell
   renders as .meter-action-empty — same footprint, no fill — so the
   four meter columns never reflow. */
.meter-action {
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0;
}

/* Nothing to show — empty placeholder. Keeps the 5-column grid stable
   across the reign (so the 4 meter columns don't reflow when gifts are
   spent or fully staged). */
.meter-action-empty {
  visibility: hidden;
}

/* Staged reign-gift row in the upper-right of the live card face. Each
   staged gift TYPE is a .card-action-card wrapper (icon + optional count
   badge); distinct types sit side-by-side with the newest to the LEFT.
   The row is absolutely positioned so it shares .card-face's positioning
   context (.card-face is position: absolute). Sized small — the staged
   gifts are a reminder, not a focal point. */
.card-action-cards {
  position: absolute;
  top: calc(0.4rem - 2px);   /* nudged up 2px to clear the card text */
  right: 0.4rem;
  z-index: 2;
  display: flex;
  flex-direction: row;
  gap: 0.35rem;
  align-items: flex-start;
}

.card-action-card {
  position: relative;
  display: block;
  width: 1.6rem;
  height: 1.6rem;
  cursor: pointer;
  filter: drop-shadow(0 1px 1px rgba(70, 50, 30, 0.2));
  transition: transform 120ms ease-out;
}
.card-action-card img {
  width: 100%;
  height: 100%;
  object-fit: contain;
  display: block;
}
.card-action-card:hover {
  transform: scale(1.1);
}

/* ===== Gift carousel (5th meter cell) =====
   Replaces the old single action-card button. Shows the selected
   available gift type centred, with ‹ › chevrons (only when 2+ distinct
   types are held) pinned to the cell edges and a count badge when 2+ of
   the selected type remain. .meter is position: relative, so the arrows
   and badge anchor to the cell. */
.gift-carousel {
  position: relative;
}
.gift-carousel-current {
  appearance: none;
  -webkit-appearance: none;
  background: transparent;
  border: none;
  border-radius: 0;
  width: 2.5rem;
  height: 2.5rem;
  padding: 0;
  cursor: pointer;
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  transition: transform 120ms ease-out;
}
.gift-carousel-current:hover {
  transform: translateY(-1px);
}
.gift-carousel-current img {
  width: 100%;
  height: 100%;
  object-fit: contain;
  display: block;
}

/* Spin chevrons — pinned to the GIFT cell's own left/right EDGES (not the
   icon centre), so the right chevron lands exactly on the cell's right
   edge = the card's right edge, and the chevrons stay WITHIN the column
   rather than overflowing it. Small + quiet; not rendered when only one
   type is held. The icon sits centred in the cell between them. */
.gift-carousel-arrow {
  appearance: none;
  -webkit-appearance: none;
  background: transparent;
  border: none;
  padding: 0;
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  width: 1.1rem;
  text-align: center;
  line-height: 1;
  font-size: 1.35rem;
  color: var(--font-color-2);
  cursor: pointer;
  opacity: 0.6;
  transition: opacity 120ms ease-out;
}
.gift-carousel-arrow:hover { opacity: 1; }
.gift-carousel-prev { left: 0; }
.gift-carousel-next { right: 0; }

/* Count badge — small numeral on a gift icon when more than one is
   available (carousel) or staged (card face). Shared by both contexts. */
.gift-count-badge {
  position: absolute;
  top: -0.25rem;
  right: -0.25rem;
  min-width: 0.95rem;
  height: 0.95rem;
  padding: 0 0.18rem;
  box-sizing: border-box;
  border-radius: 0.5rem;
  background: var(--font-color-1);
  color: var(--background-color-3);
  font-family: var(--font-family-2);
  font-size: 0.62rem;
  font-weight: 700;
  line-height: 0.95rem;
  text-align: center;
  pointer-events: none;
}

/* Roll-in animation when the centred gift changes (a spin, or the next
   gift rolling into place after a stage). Two directions so the gift
   slides in from the side the spin came from. */
@keyframes gift-roll-next {
  from { transform: translateX(40%); opacity: 0; }
  to   { transform: translateX(0); opacity: 1; }
}
@keyframes gift-roll-prev {
  from { transform: translateX(-40%); opacity: 0; }
  to   { transform: translateX(0); opacity: 1; }
}
.gift-carousel-current.gift-roll-next { animation: gift-roll-next 180ms ease-out; }
.gift-carousel-current.gift-roll-prev { animation: gift-roll-prev 180ms ease-out; }

/* ===== Gift-presentation modal =====
   A small dedicated modal shown when the ruler earns a mid-reign gift
   (e.g. a child marries into another house). It reuses the throne
   picker's tile styling (.throne-action-*) wholesale; this block only
   owns the modal's column layout + CTA spacing. The custom subtitle is
   rendered in the .throne-action-prompt slot. */
.gift-modal {
  display: flex;
  flex-direction: column;
  align-items: center;
}
.gift-modal .throne-action-picker {
  width: 100%;
}
.gift-modal-cta {
  margin-top: 1rem;
}

/* The gift modal is nudged DOWN a little from center so the ruler's
   trait pills near the top of the play screen peek out above it — the
   player reads which traits are active to weigh which gift to take. Just
   a small offset (the modal stays centered horizontally); lighten the
   dim wash so the revealed traits read clearly. */
.modal-backdrop:has(.modal-gift) {
  background: rgba(42, 36, 30, 0.22);
}

/* A touch wider than the default modal (but inside the 480px game
   container, not flush to its edges) with tighter side margins, so the
   four gift tiles render as compact squares matching the New Reign
   picker. Width override lives here; the square tiles are set on
   .gift-modal .throne-action-card below. */
.modal-container.modal-gift {
  max-width: 420px;
  padding-left: 1.25rem;
  padding-right: 1.25rem;
  /* Small downward shift from the centered position so the ruler traits
     behind it stay visible — not a full bottom-anchor, just ~100px. */
  margin-top: 100px;
}
.gift-modal .throne-action-card {
  aspect-ratio: 1;
  justify-content: center;
  padding: 0.35rem 0.2rem;
  gap: 0.2rem;
}
/* Breathing room between the gift subtitle ("…with a gift.") and the
   "(To be played…)" footnote — a clear line break, gift modal only. */
.gift-modal .throne-action-prompt {
  margin-bottom: 0.75rem;
}

/* ===== Throne modal — action card picker ===== */
.throne-action-picker {
  margin: 0.75rem 0 0.5rem;
  text-align: center;
}

.throne-action-prompt {
  margin: 0 0 0.15rem;
  font-family: var(--font-family-2);
  color: var(--font-color-2);
  font-size: 1.0rem;
}

/* Sub-prompt — quieter clarifier under the gift prompt explaining when
   the gift can be played. Smaller + italicised so it reads as a footnote
   to the instruction above, not a second instruction. */
.throne-action-subprompt {
  margin: 0 0 0.5rem;
  font-family: var(--font-family-2);
  color: var(--font-color-2);
  font-size: 0.9rem;
  font-style: italic;
  opacity: 0.85;
}

.throne-action-row {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 0.5rem;
}

/* Gift tiles read as miniature play-cards: white fill, the same 10px
   rounded corners and lifted drop-shadow the in-game .card-face carries,
   no chrome border. Selection deepens the shadow and adds a gold ring so
   the chosen gift reads as "picked up." */
.throne-action-card {
  appearance: none;
  -webkit-appearance: none;
  background: var(--background-color-3);
  border: none;
  border-radius: 10px;
  padding: 0.6rem 0.3rem;
  cursor: pointer;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.3rem;
  box-shadow: 0 3px 8px rgba(70, 50, 30, 0.22);
  transition: transform 120ms ease-out, box-shadow 120ms ease-out;
}
/* Hover + selected both slightly enlarge the tile on all axes (scale)
   rather than lifting it on the Y axis. Selected additionally carries
   the gold outline ring. transform-origin: center keeps the growth even
   on all sides. */
.throne-action-card:hover {
  transform: scale(1.06);
  box-shadow: 0 5px 12px rgba(70, 50, 30, 0.28);
}
.throne-action-card.selected {
  transform: scale(1.06);
  box-shadow: 0 0 0 2px rgba(184, 152, 96, 0.7), 0 5px 12px rgba(70, 50, 30, 0.28);
}

.throne-action-card-icon {
  width: 2.4rem;
  height: 2.4rem;
  object-fit: contain;
}

.throne-action-card-name {
  font-family: var(--font-family-2);
  font-size: 0.8rem;
  color: var(--font-color-1);
}

.throne-action-error {
  min-height: 1rem;
  margin: 0.25rem 0 0;
  font-size: 0.8rem;
  text-align: center;
}
.throne-action-error:empty {
  display: none;
}

/* ===== Badges tab ===== */
.badges-subtitle {
  margin: 0 0 0.75rem;
  font-family: var(--font-family-2);
  font-size: 0.85rem;
  font-style: italic;
  color: var(--font-color-2);
  text-align: center;
}
.badges-body {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  padding-right: 0.25rem;
}
.badges-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
}

/* One badge per row: icon · (name + description) · points. Unearned rows
   read as dimmed placeholders; earning a badge lights it up (full
   opacity, color icon). */
.badge-row {
  display: flex;
  align-items: center;
  gap: 0.7rem;
  padding: 0.55rem 0.7rem;
  background: var(--background-color-3);
  border-radius: 8px;
  box-shadow: 0 2px 6px rgba(70, 50, 30, 0.14);
  opacity: 0.5;
  transition: opacity 120ms ease-out;
}
.badge-row.earned {
  opacity: 1;
}
/* Icon wrapper — positioning context so the count pill can overlay the
   badge icon, centered. */
.badge-icon-wrap {
  position: relative;
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  justify-content: center;
}
.badge-row-icon {
  width: 3rem;
  height: 3rem;
  object-fit: contain;
  display: block;
  filter: grayscale(1);
}
.badge-row.earned .badge-row-icon {
  filter: none;
}
.badge-row-text {
  flex: 1 1 auto;
  min-width: 0;
}
.badge-row-name {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  font-family: var(--font-family-2);
  font-size: 0.95rem;
  color: var(--font-color-1);
}
/* Count pill — overlays the badge icon, centered. Inverted treatment:
   white fill, thin black outline, black text (vs. the dark-fill pill it
   replaced beside the name). */
.badge-count {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  font-family: var(--font-family-2);
  font-size: 0.72rem;
  font-weight: 700;
  color: #000;
  background: #fff;
  border: 1px solid #000;
  border-radius: 0.6rem;
  padding: 0 0.35rem;
  line-height: 1.05rem;
  pointer-events: none;
}
.badge-row-desc {
  font-family: var(--font-family-3);
  font-size: 0.8rem;
  color: var(--font-color-2);
  margin-top: 0.1rem;
}

/* ===== Badge fly-up moment ===== */
/* A celebratory toast that rises + fades over the card stage the instant
   a badge is earned mid-reign. Anchored to the card area's positioning
   context (the stage is position: relative). Not tied to a meter cell,
   unlike .meter-upkeep-toast. */
.badge-toast {
  position: absolute;
  left: 50%;
  top: 0;
  /* Anchored at the top of the card stage. translate(-50%) centers the
     toast horizontally on the left:50% anchor. translate(0, -30%) lifts
     it by ~30% of its own height, so it sits mostly on the card (~70%)
     with just a small portion peeking above the top edge — a slight
     overlap, not a full straddle. The transform stays constant — the
     toast fades in and out in place, no upward motion. */
  transform: translate(-50%, -30%);
  z-index: 6;
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  padding: 0.4rem 0.8rem;
  background: var(--background-color-1);
  border: 1px solid #c8b890;
  border-radius: 999px;
  box-shadow: 0 4px 14px rgba(70, 50, 30, 0.3);
  font-family: var(--font-family-2);
  font-size: 0.95rem;
  color: var(--font-color-1);
  white-space: nowrap;
  pointer-events: none;
  opacity: 0;
  transition: opacity 220ms ease-out;
}
.badge-toast.visible {
  opacity: 1;
}
.badge-toast img {
  width: 1.4rem;
  height: 1.4rem;
  object-fit: contain;
}

/* ===== Badge tally beat (death / end-of-dynasty modals) ===== */
/* Starts hidden; the modal adds .visible as a beat after the Legacy
   odometer settles, so the badges + bonus reveal as their own moment. */
.badge-tally {
  margin: 0.75rem 0 0.25rem;
  text-align: center;
  opacity: 0;
  transform: translateY(6px);
  transition: opacity 260ms ease-out, transform 260ms ease-out;
}
.badge-tally.visible {
  opacity: 1;
  transform: translateY(0);
}
.badge-tally-title {
  margin: 0 0 0.4rem;
  font-family: var(--font-family-2);
  font-size: 0.9rem;
  color: var(--font-color-2);
}
.badge-tally-row {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  margin: 0.15rem 0.35rem;
  padding: 0.25rem 0.55rem;
  background: var(--background-color-3);
  border-radius: 999px;
  font-family: var(--font-family-2);
  font-size: 0.85rem;
  color: var(--font-color-1);
}
.badge-tally-row img {
  width: 1.3rem;
  height: 1.3rem;
  object-fit: contain;
}
.badge-tally-row .badge-tally-pts {
  font-size: 0.75rem;
  color: var(--font-color-2);
}
