/* AGON — Mediterranean design system (from agon_mediterranean_aesthetic/DESIGN.md) */
@import url('https://fonts.googleapis.com/css2?family=Libre+Caslon+Text:wght@400;600;700&family=Source+Sans+3:wght@400;600;700&display=swap');

:root {
  --surface: #fbfaee;
  --surface-dim: #dbdbcf;
  --surface-low: #f5f4e8;
  --surface-cont: #efeee3;
  --surface-high: #e9e9dd;
  --surface-highest: #e4e3d7;
  --on-surface: #1b1c15;
  --on-surface-var: #554336;
  --inverse-surface: #303129;
  --inverse-on-surface: #f2f1e5;
  --outline: #887364;
  --outline-var: #dbc2b0;
  --primary: #8d4b00;
  --primary-cont: #b15f00;
  --primary-fixed: #ffdcc3;
  --secondary: #1c6390;
  --secondary-cont: #90cdff;
  --tertiary: #904821;
  --error: #ba1a1a;
  --gold: #b8860b;
  --gold-bright: #d4a017;
  --serif: 'Libre Caslon Text', 'Iowan Old Style', Georgia, serif;
  --sans: 'Source Sans 3', 'Helvetica Neue', Arial, sans-serif;
}

* { box-sizing: border-box; margin: 0; padding: 0; }
html, body { height: 100%; }
body {
  font-family: var(--sans);
  font-size: 16px; line-height: 1.5;
  color: var(--on-surface);
  background: var(--surface);
  overflow-x: hidden;
}
/* Per-route full-page backdrop (July 29 2026 — "background screens" for
   Raid/War/Shop/Koinon/Army). navigate() in app.js sets/clears the actual
   background-image inline (one map in assets.js's PAGE_BGS, not five CSS
   blocks); this class just supplies the sizing/positioning so it reads as a
   proper fixed hero backdrop rather than a tiled or stretched image. Panels
   stay their normal opaque parchment on top — the art shows through in the
   margins/gutters around them, same idea as the topbar's bordered backdrop. */
.has-page-bg { background-size: cover; background-position: center; background-attachment: fixed; }
/* Aug 11 2026 — the same treatment for the bare routes (guide, FAQ, news,
   legal). Separate class because .has-page-bg also re-pads `.main`, and a bare
   route has no `.main` to pad. */
.has-bare-bg { background-size: cover; background-position: center; background-attachment: fixed; }
/* Shared body-level ambient video layer (round 30-follow-up-20) — see
   setPageBgVideo() in app.js and PAGE_BG_VIDEOS in assets.js. Fixed +
   negative z-index puts both behind #app (which has no z-index of its own,
   so it stacks at the default level above these) regardless of the
   prepend/append order used to insert them; .page-bg-video sits above the
   plain body background-image, .page-bg-scrim sits above that to darken it
   for panel text, matching the gradient that used to be baked directly into
   body.style.backgroundImage before a video existed for any route. */
/* Aug 7 2026 — the shared backdrop is now a drifting camera rather than a
   flat plate, so the world hub and the raid page (which share it) both move.
   Overscanned past every edge so the pan can never expose a border, and the
   STILL image rides the same layer as the video: if the clip never decodes,
   the picture still drifts. Scale stays close to 1 so the framing remains the
   full vista the world hub always showed — the raid page's old bespoke layer
   pushed to 1.32 and cropped hard into the scene, which is the difference the
   two pages had grown apart on. */
.page-bg {
  position: fixed; inset: -6%; z-index: -2; pointer-events: none;
  transform-origin: 50% 58%;
  /* 18s, and a 1.04-1.20 scale: measured, that is ~12px/s of corner travel —
     unmistakably moving, while topping out well short of the 1.32 the old
     raid-only layer used, which cropped so far into the scene that the two
     pages looked like different pictures. */
  animation: page-bg-drift 18s linear infinite alternate;
  will-change: transform;
}
@keyframes page-bg-drift {
  from { transform: scale(1.04) translate3d(-3%, 1.6%, 0); }
  to   { transform: scale(1.20) translate3d(3%, -2%, 0); }
}
.page-bg-still {
  position: absolute; inset: 0;
  background-position: center; background-size: cover; background-repeat: no-repeat;
}
.page-bg-video {
  position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover;
  pointer-events: none;
}
@media (prefers-reduced-motion: reduce) {
  .page-bg { animation: none; transform: scale(1.06); }
}
.page-bg-scrim {
  position: fixed; inset: 0; z-index: -1; pointer-events: none;
  background: linear-gradient(rgba(20, 15, 8, .35), rgba(20, 15, 8, .45));
}

h1, h2, h3, .serif { font-family: var(--serif); letter-spacing: -0.02em; }
h1 { font-size: 32px; font-weight: 700; }
h2 { font-size: 24px; font-weight: 600; }
h3 { font-size: 18px; font-weight: 600; }
.label-caps {
  font-size: 12px; font-weight: 700; letter-spacing: 0.1em;
  text-transform: uppercase; color: var(--on-surface-var);
}
button { font-family: var(--sans); cursor: pointer; }

/* ----- stone buttons ----- */
.btn {
  --backdrop: 234, 216, 196;   /* its own pale gradient, for the contrast audit */
  display: inline-flex; align-items: center; gap: 6px; justify-content: center;
  padding: 8px 18px; border-radius: 4px;
  background: linear-gradient(180deg, #f3ede0, #e2d8c4);
  border: 1px solid var(--outline);
  box-shadow: inset 0 0 0 2px rgba(184, 134, 11, 0.35), 0 2px 0 rgba(85, 67, 54, 0.25);
  color: var(--on-surface); font-weight: 600; font-size: 15px;
  transition: box-shadow .15s, transform .05s;
  text-decoration: none;
}
.btn:hover { box-shadow: inset 0 0 0 2px var(--gold-bright), 0 2px 0 rgba(85,67,54,.25); }
.btn:active { transform: translateY(1px); box-shadow: inset 0 0 0 2px var(--gold-bright); }
.btn:disabled { opacity: .45; cursor: not-allowed; }
.btn.primary {
  --backdrop: 158, 85, 0;
  background: linear-gradient(180deg, var(--primary-cont), var(--primary));
  color: #fffbff; border-color: #6e3900;
}
.btn.blue { --backdrop: 37, 110, 160; background: linear-gradient(180deg, #2d7cb0, var(--secondary)); color: #fff; border-color: #025783; }
.btn.danger { --backdrop: 200, 44, 44; background: linear-gradient(180deg, #d43c3c, var(--error)); color: #fff; border-color: #93000a; }
.btn.small { padding: 4px 12px; font-size: 13px; }
.btn.tiny { padding: 2px 9px; font-size: 12px; }

/* Quantity input base (Army page unit cards) — the actual Train button is now the
   real carved-marble plaque graphic (.btn-plaque-train below), not a CSS button. */
.unit-card-actions { display: flex; align-items: center; gap: 8px; flex-shrink: 0; }
.qty-input {
  width: 54px; text-align: center;
  border-radius: 4px;
  padding: 8px 4px;
}
/* Hide the browser's native up/down spin-button arrows (July 29 2026, round 6 —
   "remove arrows near the number... leave only the number field and arrows on the
   sides") — the left/right stepper buttons (.qty-step below) are the only way to
   change the quantity by click now; the field itself stays plain type=number for
   its numeric keyboard/validation behavior, just without the built-in spinner UI. */
.qty-input::-webkit-outer-spin-button, .qty-input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
.qty-input[type=number] { -moz-appearance: textfield; appearance: textfield; }

/* ----- parchment panels ----- */
.panel {
  background: var(--surface-low);
  background-image: radial-gradient(ellipse at 30% 20%, rgba(255,255,255,.5), transparent 60%);
  border: 1px solid var(--outline-var);
  border-radius: 6px;
  box-shadow: inset 0 0 24px rgba(219, 194, 176, 0.25), 0 4px 14px rgba(28, 99, 144, 0.12);
  padding: 20px;
  position: relative;
}
.panel.marble {
  background: linear-gradient(165deg, #ffffff, var(--surface-highest));
  box-shadow: 0 10px 30px rgba(28, 99, 144, 0.2);
  border-color: var(--outline);
}
.panel-corner::before, .panel-corner::after {
  content: '❦'; position: absolute; color: var(--outline-var); font-size: 13px;
}
.panel-corner::before { top: 6px; left: 10px; }
.panel-corner::after { bottom: 6px; right: 10px; transform: rotate(180deg); }

.gold-frame { box-shadow: 0 0 0 1px var(--gold-bright), inset 0 0 18px rgba(212, 160, 23, .15); }

/* Dark variant of .panel — used for the Home Army / Garrison / Legendary units
   panels on the Army page, matching the near-black side panels in the user-supplied
   Army page mockup (stitch_agon_strategy_game_ui-19/screen.png). Text/label colors
   need their own overrides since the base .panel styles assume a light background. */
.panel.dark {
  background-color: #100a06;
  background-image: linear-gradient(160deg, #1c140d, #100a06);
  border-color: #3a2717;
  color: #eee6d3;
  box-shadow: inset 0 0 24px rgba(0, 0, 0, .3), 0 4px 14px rgba(0, 0, 0, .35);
}
.panel.dark h3 { color: #f3ecd8; }
.panel.dark .muted { color: #b9ac95; }
.panel.dark .label-caps { color: #d8c9a8; }
.panel.dark.panel-corner::before, .panel.dark.panel-corner::after { color: #9a7c56; }

/* Army page: stone-tablet page banner, card-grid Train Troops list, and bronze
   medallion rosters — all matching the user-supplied Army page mockup. */
.page-banner {
  display: flex; flex-direction: column; align-items: center; text-align: center;
  gap: 4px;
  background: linear-gradient(180deg, #ddd6c8, #b6ad9b 55%, #a49a86);
  border: 1px solid #8a8070; border-radius: 6px;
  padding: 14px 32px 16px;
  box-shadow: inset 0 0 0 1px rgba(255,255,255,.25), inset 0 0 30px rgba(0,0,0,.12), 0 3px 10px rgba(0,0,0,.25);
}
.page-banner h1 { color: #2b241c; }
.page-banner .meta {
  display: flex; gap: 18px; flex-wrap: wrap; justify-content: center;
  font-size: 12px; font-weight: 700; letter-spacing: .1em; text-transform: uppercase;
  color: #4b4436;
}
/* Unit-housing meter (July 29 2026, round 6 — "on top of the page create a bar
   that will display how many unit spots are already occupied and how much are
   currently training") — a two-segment progress bar in the page banner: a solid
   segment for already-occupied housing (home+garrison stacks) and a diagonally-
   hatched segment for housing reserved by the training queue, out of total
   Stratopedon capacity. `.full` (housing at/over cap) adds a warning ring. */
.housing-meter { width: 100%; max-width: 640px; margin: 10px auto 0; }
.housing-meter-head {
  display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap;
  gap: 8px; margin-bottom: 6px;
}
.housing-meter-nums {
  font-size: 12px; font-weight: 700; letter-spacing: .04em; color: #4b4436;
  display: flex; gap: 6px; align-items: center;
}
.hm-occ { color: var(--primary); }
.hm-train { color: var(--gold); }
.hm-sep, .hm-cap { opacity: .65; }
.housing-meter-track {
  position: relative; height: 14px; border-radius: 7px; overflow: hidden;
  background: rgba(0, 0, 0, .15);
  box-shadow: inset 0 1px 4px rgba(0, 0, 0, .3);
}
.housing-meter-fill { position: absolute; top: 0; bottom: 0; height: 100%; transition: width .2s, left .2s; }
.hm-fill-occ { left: 0; background: linear-gradient(180deg, var(--primary-cont), var(--primary)); z-index: 2; }
.hm-fill-train {
  background: repeating-linear-gradient(135deg, var(--gold-bright) 0 8px, var(--gold) 8px 16px);
  z-index: 1;
}
.housing-meter.full .housing-meter-track { box-shadow: inset 0 1px 4px rgba(0, 0, 0, .3), 0 0 0 2px var(--error); }

.unit-cards { display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); gap: 16px; }
/* Fixed 3-across layout for Train Troops (bigger cards, July 29 2026 — was 4-across,
   user asked for fewer/bigger per row) — explicit repeat(3,...) rather than auto-fill,
   so it's always exactly 3 per row on normal desktop widths instead of however many
   180px slots happen to fit. */
.unit-cards-3 { grid-template-columns: repeat(3, 1fr); gap: 22px; }
@media (max-width: 1100px) { .unit-cards-3 { grid-template-columns: repeat(2, 1fr); } }
@media (max-width: 640px) { .unit-cards-3 { grid-template-columns: 1fr; } }
/* Unit card, restyled (July 28 2026) to match a user-supplied reference screenshot:
   light marble/parchment body instead of the earlier dark maroon version, a serif
   name + italic tier/role subtitle, a thin gold divider, a SQUARE portrait frame next
   to a stacked labeled-stat list, another divider, then cost + the real carved-marble
   "TRAIN" plaque graphic (see .btn-plaque-train) instead of a CSS-drawn button.
   July 29 2026: the plain 1px border + CSS-only gold-fade dividers were replaced with
   a real bronze/verdigris Greek-key marble frame (`unit_card_frame.png`, a user-supplied
   "vertical unit card frame ... matching the [Agon aesthetic]" asset — a blank card
   border with an ornate meander band across the top and plainer double-line bronze
   edges on the other three sides) via a 9-slice `border-image`. `border-image-repeat:
   round stretch` tiles the top/bottom meander band to whole repeats at any card width
   (avoiding the squish/stretch distortion from the July 29 Train-button fix) while the
   plain left/right edges just stretch vertically, which is imperceptible since they
   carry no repeating motif. No `fill` keyword — the interior stays this card's own
   light gradient rather than the source art's marble texture, so card height can vary
   without a stretched-texture look. */
.unit-card {
  display: flex; flex-direction: column;
  background: linear-gradient(165deg, #ffffff, var(--surface-highest));
  border-style: solid; border-color: var(--outline);
  border-width: 46px 22px 26px 22px;
  border-image-source: url(/assets/ui/unit_card_frame.png?v=32);
  border-image-slice: 113 45 53 48;
  border-image-width: 46px 22px 26px 22px;
  border-image-repeat: round stretch;
  border-image-outset: 0;
  /* Aug 8 2026 — "remove white on the corner so the corners are round": the
     white was flood-filled out of unit_card_frame.png, which only helps if the
     card stops painting its own background underneath. padding-box does that;
     border-box (the default) would show the gradient through the new holes. */
  background-clip: padding-box;
  border-radius: 8px; overflow: hidden;
  box-shadow: 0 4px 10px rgba(28, 99, 144, .12), inset 0 0 20px rgba(219, 194, 176, .2);
  transition: transform .1s;
}
.unit-card-locked { opacity: .5; }
/* Aug 8 2026 — "audit the spacing of the text in the frame as it is too close
   to borders": every inner edge gained room, so nothing sits against the
   carved border any more. */
.unit-card-head { padding: 18px 22px 12px; text-align: center; }
/* Card font/image sizing bumped up a step (July 29 2026, round 2 — user asked for
   bigger font + images inside the frame so there's less unused/blank space between
   the content and the ornate border) — name, portrait, stat text/icons and cost
   text all sized up together so the card content fills more of the frame. */
.unit-card-name { font-family: var(--serif); font-weight: 700; font-size: 25px; color: var(--on-surface); }
.unit-card-sub { font-style: italic; font-size: 14px; color: var(--on-surface-var); margin-top: 3px; }
/* Greek-key-flavored divider: plain gold rule flanked by a repeating meander glyph —
   a CSS-only stand-in for an ornate bordered-frame asset, which wasn't supplied. */
.unit-card-divider {
  height: 1px; margin: 0 22px;
  background: linear-gradient(90deg, transparent, var(--outline-var) 15%, var(--outline-var) 85%, transparent);
}
.unit-card-main { display: flex; gap: 18px; align-items: flex-start; padding: 18px 24px 20px; flex: 1; }
.unit-card-portrait {
  flex-shrink: 0; width: 140px; height: 140px;
  background: var(--surface-cont);
  border: 2px solid var(--outline); border-radius: 4px;
  box-shadow: 0 0 0 3px var(--surface-low), 0 0 0 4px var(--outline-var), inset 0 2px 6px rgba(85, 67, 54, .18);
  display: flex; align-items: center; justify-content: center; padding: 8px;
}
.unit-card-portrait img { width: 100%; height: 100%; object-fit: contain; }
/* Left column: portrait, then (July 29 2026, round 3) a compact price/unit +
   housing strip directly underneath it, per the user's "right underneath the
   models there is price per unit + housing" request — pulled out of the
   characteristics list on the right so it reads as a quick at-a-glance summary
   right next to the unit's picture instead of just another row in the stat list. */
.unit-card-left { display: flex; flex-direction: column; align-items: center; gap: 8px; flex-shrink: 0; width: 140px; }
.unit-card-quickstats { display: flex; flex-direction: column; gap: 6px; width: 100%; }
.qs-item {
  display: flex; align-items: center; justify-content: center; gap: 6px; flex-wrap: wrap;
  background: rgba(122, 99, 78, .1); border-radius: 4px; padding: 5px 6px;
  font-size: 13px; font-weight: 700; color: #3d2e1c;
}
/* Purchase-cost chip under the portrait (July 29 2026, round 4 — user asked for the
   resources required for purchase to read bigger here) — its own full-width row,
   sized up above the base `.qs-item` text/icon size. */
.qs-item.qs-price { font-size: 16px; }
.qs-item.qs-price .cost { font-size: 16px; gap: 7px; justify-content: center; flex-wrap: wrap; }
.qs-item.qs-price .cost img { width: 24px; height: 24px; }
/* Housing + training time (time added in round 4) share a smaller row underneath the
   price chip — both are a single short number/value, so they don't need the bigger
   treatment the price chip got. */
.qs-row { display: flex; gap: 6px; width: 100%; }
.qs-row .qs-item { flex: 1; font-size: 12px; padding: 5px 4px; }
.qs-ico { font-size: 16px; line-height: 1; }
/* Real carved-icon art (July 29 2026, round 5) replacing the ⏱/🏃/⚡/⛺ emoji stand-ins
   used throughout the quickstats strip and the footer's total-housing/total-time
   chips — sized to sit comfortably next to the small value text in those spots. */
.qs-ico-img { width: 18px; height: 18px; object-fit: contain; flex-shrink: 0; vertical-align: middle; }
/* Right column: the characteristics list, back to a single row per item (July 29
   2026, round 4 — "icon, measure and on the right value") — icon on the left, stat
   name next to it, value pushed to the far right. Still a vertical list (not a
   grid) so each row keeps the full column width available. */
.unit-card-stats-tiles { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 8px; align-self: stretch; }
.stat-tile {
  display: flex; align-items: center; gap: 10px;
  background: rgba(122, 99, 78, .08); border-radius: 4px; padding: 6px 12px;
}
.stat-tile-ico {
  flex-shrink: 0; width: 30px; height: 30px;
  display: flex; align-items: center; justify-content: center; font-size: 24px; line-height: 1;
}
.stat-tile-ico img { width: 100%; height: 100%; object-fit: contain; }
.stat-tile-label { flex: 1; min-width: 0; font-size: 14px; font-weight: 700; color: #7a634e; }
.stat-tile-value { flex-shrink: 0; font-size: 17px; font-weight: 800; color: #3d2e1c; }
/* Footer split into two side-by-side columns (July 29 2026, round 6 — "on the left
   side under total for put resources and on the right side numbers with arrows and
   underneath button trained"): `.foot-totals` (left, flexible width) stacks the
   "Total for N" label, the resource-cost total, and the housing/time totals;
   `.foot-controls` (right, fixed width) stacks the qty stepper row above the Train
   button so both share the same width. */
.unit-card-foot { padding: 14px 24px 20px; display: flex; flex-direction: row; align-items: flex-start; gap: 16px; }
.foot-totals { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 7px; font-size: 14px; color: var(--on-surface-var); }
.foot-totals .cost-label { flex-shrink: 0; font-size: 13px; }
/* Total-for resource icons/numbers sized up a step (July 29 2026, round 7 —
   "make the total for icons and number bigger") — bumped from the round-4 sizing
   (15px text / 22px icons) so the actual resource cost for the chosen quantity
   reads as the most prominent figure in the footer. `flex-wrap: wrap` (already
   present) keeps Spartiate's 3-resource total from overflowing at this bigger size. */
.foot-totals .cost { font-size: 20px; gap: 9px; flex-wrap: wrap; }
.foot-totals .cost img { width: 30px; height: 30px; }
.foot-totals .qty-echo { color: var(--primary); font-weight: 700; }
.total-extras { display: flex; align-items: center; gap: 14px; flex-wrap: wrap; font-weight: 600; }
/* Total housing spaces needed for the chosen quantity (July 29 2026, round 4 —
   "add how many unit spaces it would take to create the character") — sits next to
   the total training time, live-updated the same way. */
.total-housing { display: inline-flex; align-items: center; gap: 4px; font-weight: 700; }
/* Total training time for the chosen quantity (July 29 2026, round 5 — "make so the
   time also changes depending on how much units are chosen to create") — sits next
   to the total-housing chip, same compact icon+value shape, live-updated the same way. */
.total-time { display: inline-flex; align-items: center; gap: 4px; }
.foot-controls { flex: 0 0 168px; display: flex; flex-direction: column; gap: 8px; }
.unit-card-actions { display: flex; gap: 10px; align-items: center; justify-content: center; }
/* Left/right quantity-stepper buttons (July 29 2026, round 5 — user-supplied carved
   marble arrow art, replacing the old bare number input as the only way to change
   quantity; round 6 — number field's own native up/down spinner hidden, see the
   base `.qty-input` rule above, so these are now the ONLY way to click-adjust the
   quantity). Circular plaque buttons flanking the number field; the art itself is
   square/round so background-size stays uniform (unlike the wide Train plaque) — a
   plain <img> sized to fill the button is simplest and avoids any stretch. */
.qty-step {
  flex-shrink: 0; width: 34px; height: 34px; padding: 0;
  border: none; background: none; border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  transition: transform .05s, filter .15s;
}
.qty-step img { width: 100%; height: 100%; object-fit: contain; }
.qty-step:hover { filter: brightness(1.12); }
.qty-step:active { transform: translateY(1px) scale(.94); }
.unit-card-actions .qty-input { flex: 0 0 54px; font-size: 16px; background: var(--surface-cont); color: var(--on-surface); border: 1px solid var(--outline-var); box-shadow: inset 0 2px 5px rgba(85, 67, 54, .18); }
.unit-card-actions .qty-input:focus { border-color: var(--primary-cont); }
/* Train button now sits BELOW the qty row instead of beside it (round 6), full
   width of the `.foot-controls` column. Fixed to the plaque art's own aspect ratio
   (background-size: auto 100% instead of the base .btn-plaque rule's 100% 100%) so
   it stays centered and undistorted rather than stretched — same technique as the
   original fix, just applied to a full-width box instead of a flex:1 one. */
.foot-controls .btn-plaque-train { width: 100%; height: 52px; background-size: auto 100%; }
.unit-card-locked-label {
  padding: 0 16px 16px; text-align: center; color: var(--on-surface-var);
}

.roster-medallions { display: flex; flex-wrap: wrap; gap: 18px 24px; }
.roster-medallion { display: flex; flex-direction: column; align-items: center; gap: 6px; position: relative; }
.roster-medallion b { font-weight: 700; font-size: 15px; color: #f0e6cf; }
.roster-medallion .donated-tag { position: absolute; top: -4px; right: -6px; font-size: 11px; }
/* Bumped up (July 29 2026 — "make the icons of the units bigger, as they are
   too small") from 52px; also used by the in-progress training rows below so
   Home Army reads consistently whether a stack is already trained or still
   training. */
.icon-frame.med-sm { width: 76px; height: 76px; }

/* In-progress training, shown inline inside the Home Army panel instead of a
   separate "Training queue" panel (July 29 2026 — "remove training queue,
   make so when troops are training they are in Home army and it shows with
   loading bar and shader how much is completed of 1 training unit"). Each row
   is its own icon + a gold-bar showing how much of the CURRENTLY-training
   single unit is done (server completes a batch one unit at a time — see the
   comment above the training-row markup in army.js) — the ticking is driven
   by startCountdowns() in town.js. */
.training-rows { display: flex; flex-direction: column; gap: 18px; margin-top: 18px; }
.training-row { display: flex; align-items: center; gap: 14px; padding: 2px 0; }
.training-row-info { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 8px; }
.training-row-head { display: flex; justify-content: space-between; align-items: baseline; gap: 12px; flex-wrap: wrap; }
/* Aug 10 2026 — "work on the font and spacing in Army": the bold row title was
   still cream from the dark-panel days, which on the parchment scroll read as
   a ghost ("5× Satyr training" in near-white on cream). Inked like the rest
   of the zone. */
.training-row-head b { color: #2a2013; font-size: 15px; letter-spacing: .01em; }
.training-row-eta { white-space: nowrap; font-size: 13px; }
.training-skip { flex-shrink: 0; }

/* ----- "gold-bar" — the shared liquid-gold progress bar (July 29 2026) -----
   Built from the user-supplied WebGL shader reference (a liquid-gold fill
   sliding through a bronze trench, with a soft glowing leading edge) — see
   goldBarHtml() in app.js for why this is a CSS animation instead of a literal
   per-instance WebGL canvas. Used for in-progress training rows (real
   progress, `pct`) and both loading screens (no real progress to show, so
   `indeterminate` sweeps a fixed segment back and forth instead). */
.gold-bar {
  position: relative; height: 20px; border-radius: 3px; overflow: hidden;
  background: linear-gradient(180deg, #241708, #140d05);
  box-shadow: inset 0 2px 5px rgba(0, 0, 0, .55);
}
.gold-bar-fill {
  position: absolute; top: 0; left: 0; bottom: 0; height: 100%;
  background: linear-gradient(90deg, #caa23c, #ffe27a 45%, #fff6d6 50%, #ffe27a 55%, #caa23c);
  background-size: 220% 100%;
  animation: goldShimmer 2.2s linear infinite;
  transition: width .5s ease;
}
/* Soft glow at the fill's leading (right) edge — the shader reference's
   "liquid gold" had a bright bloom at its advancing front; approximated here
   with a small blurred radial gradient riding along with the fill's own edge. */
.gold-bar-fill::after {
  content: ''; position: absolute; top: 0; right: -8px; bottom: 0; width: 16px;
  background: radial-gradient(circle, rgba(255, 230, 165, .95), transparent 70%);
  filter: blur(1.5px);
}
@keyframes goldShimmer { from { background-position: 0% 0; } to { background-position: -220% 0; } }
/* Indeterminate mode (loading screens — no real progress fraction to show): a
   fixed-width segment sweeps end to end on a loop instead of filling to a set
   width; the shimmer keeps running underneath it the same as the determinate case. */
.gold-bar.indeterminate .gold-bar-fill {
  width: 38%; transition: none;
  animation: goldShimmer 2.2s linear infinite, goldSweep 1.5s ease-in-out infinite;
}
@keyframes goldSweep { 0% { left: -38%; } 100% { left: 100%; } }
/* Framed variant — wraps the bar in the ornate marble/meander bar_frame.png
   border-image (see the unit-card-frame comment above for the identical
   9-slice technique: `round` on the top/bottom meander edges so the motif
   always tiles to whole repeats regardless of bar width, `stretch` on the
   plain marble left/right edges since they carry no repeating pattern). Used
   for anything with enough width to show the frame off — training rows,
   loading screens — small inline bars (topbar resource meters) skip it. */
.gold-bar-framed {
  height: 34px;
  border-style: solid; border-width: 8px 15px;
  border-image-source: url(/assets/ui/bar_frame.png?v=7);
  border-image-slice: 36 71 36 71;
  border-image-width: 8px 15px;
  border-image-repeat: round stretch;
  border-image-outset: 0;
}
.gold-bar-lg { height: 40px; }
.gold-bar-lg.gold-bar-framed { height: 46px; border-width: 10px 18px; border-image-width: 10px 18px; }

/* Configure-garrison button + preset-orders select, side by side (July 29 2026 —
   used to be a standalone button above a separately-labeled orders block below it). */
.gar-controls { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }

/* Aug 7 2026 — the legendary cards huddled at the left of a wide panel with
   dead space beside them. auto-fill leaves empty tracks; auto-fit collapses
   them, so two units spread across the panel instead of hugging one corner. */
.legendary-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); gap: 16px; justify-items: center; }
.legendary-card { display: flex; flex-direction: column; align-items: center; gap: 6px; }
.legendary-card b { font-size: 12px; text-align: center; color: #f0e6cf; }
.legendary-card .label-caps { color: #b9ac95; }

/* ----- layout ----- */
#app { min-height: 100vh; display: flex; flex-direction: column; }
/* Hard ceiling for flush routes (Town/Campaign/Raid) — see the `flush-page`
   toggle in app.js's navigate() for why `min-height:100vh` alone isn't
   enough: it's a floor, not a cap, so oversized content downstream (a long
   Raid battle-report list) grows #app right past the viewport instead of
   being clipped/scrolled by the internal `overflow:hidden`/`flex:1;min-
   height:0` chain those pages rely on. */
body.flush-page #app { height: 100vh; overflow: hidden; }

/* Top bar: bare logo top-left, framed resource strip centered on the bar as a whole
   ("top middle"). Nav now lives in its own fixed dock at the bottom-middle of the
   screen (see .bottom-nav below) instead of competing for space up here, so the
   resource bar can just be simply/truly centered via absolute positioning against
   `.topbar` (position: sticky/fixed both establish a containing block). */
.topbar {
  position: sticky; top: 0; z-index: 50;
  display: flex; align-items: center; flex-wrap: wrap;
  gap: 16px; min-height: 98px;
  padding: 12px 24px;
  background: #f5f4e8;
  border-bottom: 3px solid #7a634e;
}
/* On the Polis screen the top bar floats transparently on top of the map instead
   of occupying its own opaque row — the map now fills the whole viewport behind it. */
.topbar.topbar-float {
  position: fixed; top: 0; left: 0; right: 0;
  background: transparent; border-bottom: none; box-shadow: none;
  pointer-events: none;
}
.topbar.topbar-float > * { pointer-events: auto; }

.bare-logo { display: flex; align-items: center; gap: 10px; flex-shrink: 0; text-decoration: none; }
/* the source render has its emblem circle sitting on a square marble tile — clipping to a
   circle crops that marble away without needing a separate cutout asset */
.bare-logo img { width: 72px; height: 72px; object-fit: cover; border-radius: 50%; flex-shrink: 0; filter: drop-shadow(0 2px 8px rgba(0,0,0,.55)); }
.bare-logo .logo-name {
  font-family: var(--serif); font-size: 15px; font-weight: 700; color: #fbfaee;
  text-shadow: 0 1px 3px rgba(0,0,0,.65), 0 0 10px rgba(0,0,0,.4);
}
/* on non-float pages the bar sits on a solid cream strip, so the light text needs to go dark */
.topbar:not(.topbar-float) .logo-name { color: #7a634e; text-shadow: none; }

/* Nav plaques: each is a single carved-nameplate graphic with its icon and
   label already baked into the art (same idea as btnCollect/btnUpgrade), so
   the button is just the image — no separate background frame or text.
   `.subnav` is the shared plaque-row styling; it's used both here (unused now
   that nav moved out of the topbar) and by `.bottom-nav` below. */
.subnav { display: flex; align-items: center; gap: 4px; flex-wrap: wrap; justify-content: center; }
.nav-plaque {
  display: flex; align-items: center; justify-content: center;
  height: 52px; flex-shrink: 0; text-decoration: none;
  transition: transform .1s, filter .15s;
}
.nav-plaque img { height: 100%; width: auto; object-fit: contain; filter: drop-shadow(0 2px 3px rgba(0,0,0,.35)); }
.nav-plaque:hover { transform: translateY(-1px); filter: brightness(1.08); }
.nav-plaque:active { transform: translateY(1px); }
.nav-plaque.active img {
  filter: drop-shadow(0 2px 4px rgba(0,0,0,.45)) drop-shadow(0 0 6px rgba(255, 214, 130, .85)) brightness(1.15) saturate(1.1);
}

/* Nav dock: fixed to the bottom-middle of the viewport (not the page flow), so it
   holds its position regardless of scroll or which route's content is showing —
   same fixed-to-viewport pattern as .signout-corner / .town-actions-right below. */
.bottom-nav {
  position: fixed; left: 50%; bottom: 16px; transform: translateX(-50%);
  z-index: 50;
  justify-content: center;
}

.rescell-bar {
  /* truly centered on the whole topbar ("top middle") — simple now that nav no
     longer shares this row and there's nothing else to stay clear of. Sized up a
     step (July 29 2026 — "make the top bar with resources a bit bigger") along
     with the icons/text/border below, to fit the new per-resource fill bars
     without feeling cramped. */
  position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%);
  display: flex; align-items: center; gap: 19px; flex-wrap: wrap;
  padding: 13px 22px;
  /* 9-slice border-image instead of a plain stretched background: the 4 corner tiles of the
     frame art render at native proportions (never stretched), only the straight edge strips
     repeat/scale to fill the gap between them — this is what stops the ornate border from
     smearing into a thin, over-stretched line on a box this much wider than it is tall. */
  border-style: solid; border-width: 30px;
  border-image: url(/assets/ui/resource_frame.png?v=5) 150 fill / 30px / 0 round;
}
.rescell { position: relative; display: flex; flex-direction: column; align-items: center; gap: 2px; }
.rescell img { width: 31px; height: 31px; object-fit: contain; filter: drop-shadow(0 1px 2px rgba(0,0,0,.3)); }
.rescell .rc-emoji { font-size: 26px; line-height: 1; filter: drop-shadow(0 1px 2px rgba(0,0,0,.3)); }
.rescell .rc-label { font-size: 10px; font-weight: 700; letter-spacing: .1em; text-transform: uppercase; color: #7a634e; }
.rescell .rc-amt { font-size: 13px; font-weight: 700; color: #3d2e1c; }
.rescell .rc-amt i { font-weight: 400; font-style: normal; color: #8a705a; }
/* Per-resource storage-fill meter (July 29 2026 — "put there bars that will
   indicate how much of the total resource capacity are filled") — a thin bar
   under each capped resource's amount, filled to current/cap. Ambrosia and
   Glory have no cap so they simply don't get one (see the JS rescell() helper).
   A red ring around the TRACK (not the fill) appears once a resource is at/near
   its cap (>=90%), flagging that further production on that resource is about
   to be wasted — kept separate from the fill color so each resource's own color
   (below) always stays visible regardless of fill level. */
/* ── The fold tab (Aug 13 2026) ───────────────────────────────────────────
   "make so I click a button and fold/unfold resource top menu".

   A pull-tab hanging off the bottom edge of the resource plaque. It is a
   sibling of .rescell-bar, not a child, because the plaque is what gets
   hidden — a tab inside it would go with it and there would be nothing left
   to click. Centred on the same axis so it reads as attached to the plaque,
   and it stays on that axis when the plaque is gone, which is what makes the
   thing findable again. */
.res-fold {
  position: absolute; left: 50%;
  /* The plaque's border box is 179px tall — the 9-slice frame is 30px on every
     side — so its bottom edge sits ~90px below the topbar's centre line, well
     past the bar itself. Measured rather than guessed: at +40px the tab landed
     on top of the ARGYRION cell. The extra 4px tucks it under the frame's
     bottom rail instead of floating free of it. */
  top: calc(50% + 94px);
  transform: translate(-50%, -50%);
  z-index: 3;
  display: flex; align-items: center; justify-content: center;
  width: 54px; height: 20px; padding: 0;
  cursor: pointer;
  /* A <button> with no background of its own paints Chrome's ButtonFace grey
     across its whole border box — the same trap that put a white frame behind
     the guide's four-things nav. Both lines are load-bearing. */
  -webkit-appearance: none; appearance: none;
  background: linear-gradient(#f2e6c9, #d9c69a);
  border: 1px solid #7a634e;
  border-radius: 0 0 9px 9px;
  border-top: none;
  box-shadow: 0 2px 6px rgba(0, 0, 0, .4);
  transition: top .18s ease, background .15s, transform .1s;
}
.res-fold .res-fold-chev {
  font-size: 10px; line-height: 1; color: #5b4630;
  /* The glyph is the only moving part, so it carries the animation. */
  transition: transform .18s ease;
}
.res-fold:hover { background: linear-gradient(#fbf3dd, #e6d4a8); }
.res-fold:active { transform: translate(-50%, -50%) translateY(1px); }
.res-fold:focus-visible { outline: 2px solid var(--gold); outline-offset: 2px; }
/* Over the map the bar is transparent, so the tab has to carry its own dark
   plate or it floats as a pale chip on the artwork. */
.topbar-float .res-fold {
  background: linear-gradient(rgba(38, 27, 12, .92), rgba(24, 17, 7, .92));
  border-color: rgba(212, 160, 23, .55);
}
.topbar-float .res-fold .res-fold-chev { color: #f0dfae; }
.topbar-float .res-fold:hover { background: linear-gradient(rgba(58, 42, 18, .95), rgba(38, 27, 12, .95)); }

/* Folded: the plaque goes, and NOTHING else moves.
   Aug 13 2026 — "please make so only resource menu folds, everything else on
   top stays the same place and size." The first cut also shrank the bar to
   60px and the emblem to 44px, on the theory that reclaiming header height was
   the point. It was not: a control that rearranges the furniture around it
   every time you press it makes the whole top of the screen feel unstable. The
   bar keeps its height and the emblem its size, so the only thing that changes
   is the one thing that was asked for.
   The tab rides up onto the bar's own centre line, because sitting where the
   vanished plaque's bottom edge used to be would leave it stranded in mid-air. */
.topbar.res-folded .rescell-bar { display: none; }
.topbar.res-folded .res-fold {
  top: 50%;
  border-radius: 9px; border-top: 1px solid #7a634e;
}
.topbar-float.res-folded .res-fold { border-top-color: rgba(212, 160, 23, .55); }
@media (prefers-reduced-motion: reduce) {
  .res-fold, .res-fold .res-fold-chev { transition: none; }
}

.rc-bar { width: 48px; height: 5px; border-radius: 3px; overflow: hidden; margin-top: 1px; background: rgba(0, 0, 0, .18); box-shadow: inset 0 1px 2px rgba(0, 0, 0, .25); }
.rc-bar.near-full { box-shadow: inset 0 1px 2px rgba(0, 0, 0, .25), 0 0 0 1px var(--error), 0 0 4px rgba(186, 26, 26, .65); }
/* Aug 8 2026 — over the cap. A bar pinned at 100% cannot say "there is more
   than this, and it is rotting", so the cell says it in words and the bar goes
   amber to catch the eye without shouting like the near-full warning does. */
.rc-bar.rc-bar-over {
  box-shadow: inset 0 1px 2px rgba(0, 0, 0, .25), 0 0 0 1px #c9772e, 0 0 5px rgba(201, 119, 46, .7);
}
.rescell.rc-over { position: relative; }
.rc-over-tag {
  display: block; font-size: 10px; font-weight: 700; letter-spacing: .04em;
  color: #c9772e; margin-top: -1px; white-space: nowrap;
  animation: rc-over-pulse 2.4s ease-in-out infinite;
}
@keyframes rc-over-pulse { 0%, 100% { opacity: .72; } 50% { opacity: 1; } }
/* "Shader" fill (July 29 2026 — user-supplied WebGL liquid-gold reference,
   re-implemented as a CSS animation rather than a literal per-bar WebGL
   canvas — see goldBarHtml()'s comment in app.js for why). `position:relative`
   +`::after` overlay a traveling glint across each resource's own colored
   gradient (kept per-resource below, unchanged from round 8) so every bar
   reads as a moving liquid rather than a flat static fill. `transition: width`
   is what actually animates old→new fill on a resource change — see
   animateResBars() in app.js, which supplies the "old" starting width since
   this whole header is rebuilt from scratch on every navigate(). */
.rc-bar-fill {
  position: relative; height: 100%; overflow: hidden;
  background: linear-gradient(180deg, var(--primary-cont), var(--primary));
  transition: width .6s cubic-bezier(.3, .7, .3, 1);
}
.rc-bar-fill::after {
  content: ''; position: absolute; inset: 0;
  background: linear-gradient(100deg, transparent 30%, rgba(255, 255, 255, .6) 48%, transparent 66%);
  background-size: 260% 100%;
  animation: rcShimmer 2.6s linear infinite;
  mix-blend-mode: overlay;
}
@keyframes rcShimmer { from { background-position: 160% 0; } to { background-position: -160% 0; } }
/* Per-resource fill colors (July 29 2026, round 8 — "sitos yellow, elaia olive
   color, meli orange, argyrion grey, marmaron white") — matched to what each
   resource actually looks like (grain, olives, honey, silver coin, marble). */
.rescell[data-res="sitos"] .rc-bar-fill { background: linear-gradient(180deg, #ffe066, #e6b800); }
.rescell[data-res="olive"] .rc-bar-fill { background: linear-gradient(180deg, #8a9a3b, #5f6e28); }
.rescell[data-res="honey"] .rc-bar-fill { background: linear-gradient(180deg, #ffb74d, #f57c00); }
.rescell[data-res="argyrion"] .rc-bar-fill { background: linear-gradient(180deg, #cfd4d8, #9aa1a8); }
.rescell[data-res="marmaron"] .rc-bar-fill { background: linear-gradient(180deg, #ffffff, #dcdcdc); box-shadow: inset 0 0 0 1px rgba(0, 0, 0, .1); }

/* Resource-change feedback (July 29 2026 — "implement shader on all resources
   when they are spent and add instead of text near the resource how much was
   added"): a floating +N/−N popup near the amount, plus a brief glow pulse on
   the whole cell — gold for a gain, warm red for a spend. Both are triggered
   per-render by resDelta()/deltaHtml() in app.js's topbar() (diffing against
   the previous render's amounts) and cleaned up by animateResBars() once their
   one-shot animation finishes. */
.rc-delta {
  position: absolute; left: 50%; top: -4px; transform: translate(-50%, 0);
  font-size: 13px; font-weight: 800; white-space: nowrap; pointer-events: none; z-index: 5;
  animation: rcDeltaFloat 1.3s ease-out forwards;
}
.rc-delta.gain { color: #ffe27a; text-shadow: 0 0 6px rgba(255, 214, 110, .9), 0 1px 2px rgba(0, 0, 0, .7); }
.rc-delta.spend { color: #ff9478; text-shadow: 0 0 6px rgba(220, 60, 40, .9), 0 1px 2px rgba(0, 0, 0, .7); }
@keyframes rcDeltaFloat {
  0% { opacity: 0; transform: translate(-50%, 6px) scale(.8); }
  15% { opacity: 1; transform: translate(-50%, -4px) scale(1.05); }
  75% { opacity: 1; transform: translate(-50%, -24px) scale(1); }
  100% { opacity: 0; transform: translate(-50%, -36px) scale(.95); }
}
.res-gain-flash .rc-bar, .res-gain-flash img, .res-gain-flash .rc-emoji { animation: resGainPulse .9s ease-out; }
.res-spend-flash .rc-bar, .res-spend-flash img, .res-spend-flash .rc-emoji { animation: resSpendPulse .9s ease-out; }
@keyframes resGainPulse { 0% { filter: brightness(1); } 30% { filter: brightness(1.7) saturate(1.3); } 100% { filter: brightness(1); } }
@keyframes resSpendPulse { 0% { filter: brightness(1); } 30% { filter: brightness(.5) saturate(1.4); } 100% { filter: brightness(1); } }

/* Below this width the absolutely-centered resource bar can overlap the logo —
   drop it back into normal flow, centered, wrapping below the logo instead. */
@media (max-width: 900px) {
  .topbar { justify-content: center; }
  .rescell-bar { position: static; transform: none; width: 100%; justify-content: center; margin-top: 4px; order: 2; }
}


.main { flex: 1; padding: 24px 32px 108px; max-width: 1400px; width: 100%; margin: 0 auto; }
/* Polis screen goes full-bleed: no max-width/padding, canvas fills the rest of the viewport */
.main-flush { flex: 1; display: flex; flex-direction: column; padding: 0; max-width: none; margin: 0; min-height: 0; }
/* Routes with a full-page backdrop (July 29 2026) now float the top bar
   transparently over them instead of it taking up its own row (see
   `.topbar-float` above and topbar()'s `floaty` check in app.js) — `body`
   only gets `.has-page-bg` on exactly those routes, so this only needs to
   bump the page's own top padding to clear the now-fixed bar, everywhere
   else (incl. Battle, which keeps the opaque in-flow bar) is untouched. */
body.has-page-bg .main { padding-top: 132px; }
/* ...but a flush route manages its own padding (.raid-page already reserves
   132px for the floating top bar). Without this, a flush page listed in
   PAGE_BGS gets both paddings and grows past the viewport — which is what
   made the raid page scrollable the first time it was tried, and why it was
   pulled out of PAGE_BGS instead of fixed. Fixed properly now. */
body.has-page-bg .main.main-flush { padding-top: 0; }

.town-root { position: relative; flex: 1; min-height: 0; }
/* Canvas fills the entire Polis screen; the header/controls float on top of it
   in a transparent overlay instead of pushing the map down as a separate bar. */
.town-canvas-wrap { position: absolute; inset: 0; min-height: 420px; overflow: hidden; }
.town-overlay {
  position: absolute; top: 0; left: 0; right: 0; z-index: 20;
  display: flex; flex-direction: column;
  pointer-events: none;
}
.town-overlay > * { pointer-events: auto; }
.town-header-strip:not(:empty) { padding: 24px 24px 0; margin-bottom: 8px; }

/* Village action buttons dock to fixed screen positions instead of sitting in the header row:
   Collect/Edit/Walls/Build float vertically centered on the right, Sign out anchors the
   bottom-right corner — both fixed to the viewport so they hold their spot regardless of
   the header's own height or the canvas underneath. */
.town-actions-right {
  position: fixed; top: 50%; right: 24px; transform: translateY(-50%); z-index: 25;
  display: flex; flex-direction: column; align-items: flex-end; gap: 10px;
}
.signout-corner { position: fixed; right: 24px; bottom: 24px; z-index: 25; }

/* ----- town scene ----- */
.town-scene {
  position: relative; width: 100%; aspect-ratio: 16/9.5; min-height: 540px;
  border-radius: 10px; overflow: hidden;
  border: 2px solid var(--outline);
  box-shadow: 0 12px 40px rgba(28, 99, 144, .25);
  background:
    radial-gradient(ellipse 55% 26% at 50% 30%, rgba(213, 201, 161, .95) 40%, transparent 75%),
    radial-gradient(ellipse 90% 55% at 50% -8%, #fff3d6 0%, transparent 55%),
    linear-gradient(180deg, #aed4e8 0%, #cfe6ef 12%, #7db3cc 17%, #3f7ea6 22%,
      #d5c9a1 27%, #c3b287 45%, #a9986e 70%, #8f8058 100%);
}
.town-scene::before { /* sun */
  content: ''; position: absolute; top: 4%; left: 12%; width: 70px; height: 70px;
  border-radius: 50%; background: radial-gradient(circle, #fff8e0, #ffe9a8 55%, transparent 72%);
  filter: blur(1px);
}
.town-scene::after { /* distant isle */
  content: ''; position: absolute; top: 17%; right: 8%; width: 130px; height: 22px;
  border-radius: 50% 50% 0 0; background: #6592a8; opacity: .6;
}
.hotspot {
  position: absolute; transform: translate(-50%, -50%);
  display: flex; flex-direction: column; align-items: center; gap: 3px;
  cursor: pointer; z-index: 2; border: none; background: none; padding: 0;
}
.hotspot .medallion {
  border-radius: 50%; object-fit: cover; display: block;
  border: 3px solid #efe6d2;
  box-shadow: 0 0 0 2px var(--outline), 0 6px 16px rgba(27,28,21,.4);
  background: var(--surface-high);
  transition: transform .12s, box-shadow .12s;
}
.hotspot:hover .medallion { transform: scale(1.06); box-shadow: 0 0 0 2px var(--gold-bright), 0 8px 20px rgba(27,28,21,.45); }
.hotspot .medallion.ph {
  display: flex; align-items: center; justify-content: center;
  font-size: 30px;
}
.hotspot .medallion.ghost {
  border-style: dashed; border-color: rgba(255,255,255,.75); opacity: .8;
  background: rgba(251, 250, 238, .35); color: rgba(27,28,21,.6);
}
.hotspot .tag {
  background: rgba(38, 50, 60, .85); color: #f2f1e5;
  border-radius: 4px; padding: 1px 8px; font-size: 11px; font-weight: 700;
  white-space: nowrap; letter-spacing: .03em;
  border: 1px solid rgba(212,160,23,.4);
}
.hotspot .lvl { color: var(--gold-bright); }
.constr-ring {
  position: absolute; inset: -3px; border-radius: 50%;
  border: 3px dashed var(--gold-bright);
  animation: spin 6s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
.constr-badge {
  position: absolute; top: -6px; right: -8px; background: var(--gold);
  color: #fff; font-size: 12px; border-radius: 999px; padding: 1px 7px; font-weight: 700;
  z-index: 3;
}

/* ----- modal ----- */
.modal-back {
  position: fixed; inset: 0; background: rgba(27, 28, 21, .55); z-index: 100;
  display: flex; align-items: center; justify-content: center; padding: 20px;
}
.modal {
  /* The parchment scroll (Aug 7 2026 — "there was a scroll model - please make
     it as menue that open when you press on the building and all the other
     manu interfaces"): every modal is drawn ON the user's scroll render via a
     9-slice border-image — wooden rollers top and bottom, torn edges at the
     sides, the plain parchment middle stretching with the content. */
  border: solid transparent;
  /* Slices sit at the measured SOLID-parchment boundary (the torn edges reach
     152px in on the sides, the meanders end at y≈168/673 of the 828×842 art),
     so the stretched middle is pure clean parchment and no tear can ever
     appear under the content (Aug 7 2026 — "Still things doen't fit"). */
  border-width: 128px 118px 130px 118px;
  border-image: url('/assets/ui/scroll_frame.png?v=38') 175 160 175 160 fill / 128px 118px 130px 118px stretch;
  background: none;
  border-radius: 0;
  box-shadow: none;
  max-width: 880px; width: 100%; max-height: 94vh;
  animation: rise .18s ease-out;
  /* The panel frame no longer scrolls — the BODY inside it does (Aug 7 2026 —
     "the buildings small menu is not a scroll"). Scrolling the whole panel
     carried the ✕ button away with it, and on macOS the overlay scrollbar is
     invisible until you happen to try, so a clipped building card just looked
     broken. The frame stays put, the content scrolls inside it, and the
     scrollbar below is always visible. */
  display: flex; flex-direction: column;
  /* overflow: hidden came off (Aug 10 2026): Safari would not paint the
     border-image scroll on a flex box that also clipped overflow — the modal
     rendered as a bare parchment slab on the user's Mac while the same
     9-slice drew fine on non-clipping elements (.battle-report, .k-scroll).
     Vertical clipping is .modal-body's job anyway. */
}
.modal .modal-body { overflow-y: auto; min-height: 0; overscroll-behavior: contain; padding: 0 8px 6px; }
/* .panel.marble's gradient and inset shadows would paint a marble slab over
   the scroll's parchment — the border-image's `fill` supplies the background
   now, so the panel chrome comes off entirely on modals. */
.panel.marble.modal { background: none; box-shadow: none; border-color: transparent; }
.modal.panel { padding: 4px 10px; }
.modal.panel-corner::before, .modal.panel-corner::after { display: none; }
.modal .close { top: 4px; right: 30px; }

/* The build tray wears the same scroll, scaled to its rail width. */
.build-tray {
  border: solid transparent !important;
  border-width: 44px 32px 46px 32px !important;
  border-image: url('/assets/ui/scroll_frame.png?v=38') 175 160 175 160 fill / 44px 32px 46px 32px stretch !important;
  background: none !important;
  border-radius: 0 !important;
  box-shadow: none !important;
}

/* Always-visible carved-bronze scrollbar for every in-game scroll area, so a
   scrollable menu LOOKS scrollable on macOS instead of hiding its thumb. */
.modal .modal-body, .tray-cards, .cart-drawer, .cart-lines { scrollbar-width: thin; scrollbar-color: #8a6a48 rgba(122,90,58,.14); }
.modal .modal-body::-webkit-scrollbar, .tray-cards::-webkit-scrollbar, .cart-drawer::-webkit-scrollbar, .cart-lines::-webkit-scrollbar { width: 9px; }
.modal .modal-body::-webkit-scrollbar-track, .tray-cards::-webkit-scrollbar-track, .cart-drawer::-webkit-scrollbar-track, .cart-lines::-webkit-scrollbar-track {
  background: rgba(122,90,58,.14); border-radius: 8px;
}
.modal .modal-body::-webkit-scrollbar-thumb, .tray-cards::-webkit-scrollbar-thumb, .cart-drawer::-webkit-scrollbar-thumb, .cart-lines::-webkit-scrollbar-thumb {
  background: linear-gradient(180deg, #a07a4e, #7a5a3a); border-radius: 8px;
  border: 1.5px solid rgba(58,42,24,.5);
}
@keyframes rise { from { transform: translateY(14px); opacity: 0; } }
.modal .close {
  position: absolute; top: 10px; right: 12px; border: none; background: none;
  font-size: 22px; color: var(--on-surface-var); cursor: pointer;
}

/* Building Progress — FULLSCREEN grid + detail panel (round 30-follow-up-9
   part 4: "make it vertical again, keep similar style, but make it more
   logical" — see the redesign comment above buildProgressGrid() in town.js
   for the full reasoning). Same append-to-body fullscreen pattern as
   .battle-loading further down this file. `.progress-body` is a row
   flexbox: a fixed-width `.progress-side` (build queue + the selected
   level's detail card) and `.progress-tree-scroll` (the grid — now rows are
   WAVES/stages, primary scroll direction is vertical) filling the rest. */
.progress-fullscreen {
  position: fixed; inset: 0; z-index: 250; background: var(--surface);
  display: flex; flex-direction: column; animation: rise .18s ease-out;
}
.progress-fullscreen-header {
  display: flex; align-items: flex-start; justify-content: space-between; gap: 16px;
  padding: 18px 22px 10px; flex: none;
}
.progress-fullscreen-header h2 { margin: 0 0 2px; }
.progress-fullscreen-header .close {
  border: none; background: none; font-size: 24px; color: var(--on-surface-var); cursor: pointer;
}
.progress-body { flex: 1; min-height: 0; display: flex; border-top: 1.5px solid var(--outline-var); }
.progress-side {
  flex: none; width: 320px; overflow-y: auto; padding: 16px;
  background: var(--surface-cont); border-right: 1.5px solid var(--outline-var);
}
.progress-fullscreen-queue { margin-bottom: 14px; }
.progress-tree-scroll { flex: 1; min-height: 0; overflow: auto; background: #eef4f8; padding: 24px; }
.progress-grid-canvas { position: relative; margin: 0 auto; }
.progress-graph-svg { display: block; position: absolute; top: 0; left: 0; }
/* Aug 11 2026 — "Make arrows white - so they look okay on wooden
   background." The tree stands on the planks now, and brown lines on brown
   wood were invisible. Both edge kinds are chalk-white, separated by weight
   and opacity rather than by colour, and each carries a dark drop shadow so
   the line still reads where it crosses a pale knot in the grain. */
.pg-edge-chain {
  fill: none; stroke: rgba(255, 250, 240, .5); stroke-width: 1.5;
  stroke-linejoin: round; stroke-linecap: round;
}
.pg-edge-req {
  fill: none; stroke: #fffaf0; stroke-width: 2.2;
  stroke-linejoin: round; stroke-linecap: round; opacity: .95;
}
.progress-graph-svg { filter: drop-shadow(0 1px 2px rgba(0, 0, 0, .75)); }
#pg-arrow path { fill: #fffaf0; }

/* Rows are WAVES (a real longest-path dependency layer, computed by
   computeLevelWaves() in town.js), not raw level numbers — a small node per
   (building, level) is placed in whichever row it first becomes reachable,
   so a row reads as "everything you could tackle at this point." Node style
   is unchanged from part 3 (small plain tile, colored status ring, no
   .icon-frame medallion border at this size, click for the detail panel).
   `.pg-stage-label` is just a faint index number down the left margin for
   orientation while scrolling a tall page — not meant to carry meaning by
   itself. */
.pg-stage-label {
  position: absolute; left: 0; width: 26px; text-align: right;
  font-size: 12px; font-weight: 800; color: #f0e2c4; opacity: .75;
  text-shadow: 0 1px 3px rgba(0, 0, 0, .8);
  pointer-events: none;
}
.pgl-node-slot { position: absolute; transform: translate(-50%, -50%); cursor: pointer; z-index: 1; }
.pgl-photo-wrap {
  position: relative; width: 54px; height: 54px; border-radius: 10px;
  background: linear-gradient(165deg, rgba(255, 251, 240, .96), rgba(232, 220, 194, .92));
  box-shadow: 0 2px 6px rgba(0, 0, 0, .5);
  transition: transform .12s ease-out, filter .12s ease-out;
}
.pgl-photo, .pgl-photo-fallback {
  position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
  width: 62%; height: 62%; object-fit: contain;
}
.pgl-photo-fallback { display: flex; align-items: center; justify-content: center; font-size: 18px; }
/* The three node overlays. Aug 11 2026 — they used to be a padlock, a hammer
   and a star typed as characters; the lock is drawn in CSS now and the other
   two carry real art (see the img rules at the foot of this file). */
.pgl-lock, .pgl-building, .pgl-star {
  position: absolute; right: -4px; bottom: -4px; line-height: 1;
}
.pgl-node-done .pgl-photo-wrap { box-shadow: 0 0 0 2px #3f8f4f; }
.pgl-node-ready .pgl-photo-wrap { box-shadow: 0 0 0 2px #b8860b, 0 0 8px 1px rgba(184, 134, 11, .55); }
.pgl-node-building .pgl-photo-wrap { box-shadow: 0 0 0 2px #2f6fb0; }
/* Aug 11 2026 — the tree stands on dark wood now, so dimming the whole TILE
   turned an unreachable building into a black square. The tile keeps its
   light face; only the picture on it goes quiet. */
.pgl-node-locked .pgl-photo-wrap { box-shadow: 0 0 0 2px rgba(160, 58, 58, .6), 0 2px 6px rgba(0, 0, 0, .5); }
.pgl-node-locked .pgl-photo { filter: grayscale(55%) brightness(.9); }
.pgl-node-future .pgl-photo-wrap { background: linear-gradient(165deg, rgba(240, 234, 220, .72), rgba(214, 203, 180, .68)); }
.pgl-node-future .pgl-photo { filter: grayscale(80%) brightness(.85); opacity: .75; }
.pgl-node-slot:hover .pgl-photo-wrap, .pgl-node-selected .pgl-photo-wrap { transform: scale(1.2); z-index: 2; }
.pgl-node-selected .pgl-photo-wrap { box-shadow: 0 0 0 3px var(--gold), 0 0 10px 2px rgba(212, 160, 23, .6); }

/* Detail card — the HoMM-style "click an icon, see its tooltip" panel: name,
   level badge, description, cost/time (when relevant), and a status line
   (round 30-follow-up-9 part 3: now keyed to the exact selected level, not
   just "whatever's next" — see levelDetailHtml() in town.js). */
.progress-detail { display: flex; gap: 12px; }
.pd-photo-wrap { flex: none; width: 72px; height: 72px; }
.pd-photo, .pd-photo-fallback { width: 66%; height: 66%; object-fit: contain; }
.pd-photo-fallback { display: flex; align-items: center; justify-content: center; font-size: 26px; }
.pd-body { min-width: 0; }
.pd-name { font-weight: 700; font-size: 15px; }
.pd-level-badge { font-weight: 600; font-size: 12px; color: var(--on-surface-var); margin-left: 4px; }
.pd-desc { font-size: 12px; color: var(--on-surface-var); margin: 2px 0 8px; }
.pd-row { display: flex; align-items: center; justify-content: space-between; gap: 8px; font-size: 12.5px; margin-bottom: 5px; }
.pd-label { color: var(--on-surface-var); }
.pd-status { font-size: 12.5px; font-weight: 600; margin-top: 8px; padding-top: 8px; border-top: 1px solid var(--outline-var); }
.pd-status-ready { color: #3f8f4f; }
.pd-status-locked { color: #a03a3a; }
.pd-status-building { color: #2f6fb0; }
.pd-status-maxed { color: var(--gold); }
/* Round 30-follow-up-12 — the full build-order chain under the status line
   (see levelFullChain()/levelDetailHtml() in town.js). A plain ordered list
   reads as steps, which is exactly what this is — "build these, in this
   order, before you can reach the level you picked." Kept visually
   secondary (smaller, muted) to the status line above it, which is still the
   headline answer ("locked", "ready", etc.) — this is the supporting detail. */
.pd-chain { margin-top: 8px; padding-top: 8px; border-top: 1px solid var(--outline-var); }
.pd-chain-label { font-size: 11.5px; font-weight: 700; color: var(--on-surface-var); text-transform: uppercase; letter-spacing: .02em; margin-bottom: 4px; }
.pd-chain-list { margin: 0; padding-left: 18px; font-size: 12.5px; color: var(--on-surface); }
.pd-chain-list li { margin-bottom: 2px; }

/* ----- loading screens (July 29 2026) ----- */
/* Full-screen "marching to battle" transition — see battleLoading() in
   app.js. Shown from the moment a raid/campaign fight is launched until the
   battle view has actually rendered; picks one of the two loading-art images
   at random each time (randomLoadingBg() in assets.js) rather than a fixed
   image per trigger, per user request. */
.battle-loading {
  position: fixed; inset: 0; z-index: 300; overflow: hidden;
  display: flex; align-items: center; justify-content: center;
  background: #140f08;
  animation: rise .25s ease-out;
}

/* ── the drifting art layer shared by both full-screen loading overlays ─────
   Aug 6 2026 — "when I click raid the background doesn't move". Both overlays
   used to paint the art as their own background, which is exactly the one
   place it cannot be animated: transforming the overlay would carry the quote
   and the progress bar with it. So the picture gets its own element, sized
   past the edges of the screen, and takes the same slow Ken Burns push the
   raid page's video already uses. The scrim is a third layer rather than a
   gradient stacked on the image, so darkening the picture never has to move
   with it. */
.loading-bg {
  position: absolute;
  /* Overscan. The drift translates up to 2.2% and the keyframes never scale
     below 1.05, so 6% of slack on every side means an edge can't creep into
     frame even mid-animation. */
  inset: -6%;
  background-size: cover; background-position: center;
  z-index: 0; pointer-events: none;
  transform-origin: 50% 58%;
  /* Deliberately NOT the raid page's 22s ease-in-out. That curve is nearly
     stationary for its first couple of seconds — measured, 1.3px of movement
     in the first 1.6s — and a loading overlay is only ever on screen for
     three or four. The viewer sees the start of this animation and nothing
     else, so it has to be moving at full speed from the first frame: linear,
     and a short enough cycle that the drift reads as a camera push rather
     than a slow zoom you have to stare at to notice. */
  animation: loading-bg-drift 14s linear infinite alternate;
}
@keyframes loading-bg-drift {
  from { transform: scale(1.06) translate3d(-3%, 1.5%, 0); }
  to   { transform: scale(1.26) translate3d(3%, -2%, 0); }
}
.loading-scrim {
  position: absolute; inset: 0; z-index: 1; pointer-events: none;
  background: linear-gradient(rgba(20, 15, 8, .5), rgba(20, 15, 8, .68));
}
/* Everything the player is meant to read sits above both layers. A positioned
   element paints over a static one whatever the DOM order, so the content
   needs its own z-index to clear the two absolute layers beneath it — named
   one by one rather than with a blanket child selector, because
   .raid-loading-foot is pinned to the bottom with its own position and a
   catch-all would outrank and undo that. */
.battle-loading-box,
.raid-loading .raid-quote { position: relative; z-index: 2; }
.raid-loading .raid-loading-foot { z-index: 2; }
/* Someone who has asked the OS for less movement gets a still picture — the
   drift is decoration, and this one is on screen while they wait. */
@media (prefers-reduced-motion: reduce) {
  .loading-bg { animation: none; transform: scale(1.06); }
}

.battle-loading-box {
  background: rgba(20, 15, 10, .55); backdrop-filter: blur(2px);
  border: 1px solid rgba(212, 160, 23, .4); border-radius: 10px;
  padding: 32px 48px; text-align: center; color: #f2ecdf;
}
/* Replaces the old spinner (July 29 2026 — "add loading bar and shader to...
   loading screens") with the shared gold-bar, indeterminate since there's no
   real progress fraction to show here, just "still waiting". */
.battle-loading-box .gold-bar { width: 320px; max-width: 60vw; margin: 0 auto 20px; }
.battle-loading-text { font-family: var(--serif); font-size: 20px; letter-spacing: .02em; }

/* Small in-panel loading card for the Raid page's opponent list (reroll-safe —
   see oppLoadingHtml() in world.js) rather than a full-screen takeover, since
   this one can fire repeatedly on every "New targets" click. */
.opp-loading {
  height: 150px; border-radius: 8px; background-size: cover; background-position: center;
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 12px;
  color: #f2ecdf;
}
.opp-loading .gold-bar { width: 220px; max-width: 70%; }
.opp-loading-text { font-size: 14px; font-weight: 600; }

/* ----- lists / rows ----- */
.rows > * + * { border-top: 1px dashed var(--gold); }
.row { display: flex; align-items: center; gap: 12px; padding: 10px 4px; }
/* Aug 10 2026 — "make sure it doesn't highlight it with blue": chosen rows
   warm up in the parchment's own gold instead of sky blue. */
.row.sel { background: rgba(212, 160, 23, .14); box-shadow: inset 3px 0 0 var(--gold); border-radius: 4px; }
.row img.icon { width: 44px; height: 44px; border-radius: 50%; object-fit: contain; border: 2px solid var(--outline-var); background: var(--surface-cont); padding: 2px; }
.row .grow { flex: 1; min-width: 0; }
.muted { color: var(--on-surface-var); font-size: 13px; }

.cost { display: inline-flex; gap: 10px; align-items: center; font-weight: 700; font-size: 14px; }
.cost span { display: inline-flex; align-items: center; gap: 4px; }
.cost img { width: 20px; height: 20px; object-fit: contain; }
.cost .lack { color: var(--error); }

/* ----- ornate medallion frame (behind unit portraits) ----- */
.icon-frame {
  position: relative; flex-shrink: 0; display: inline-flex; align-items: center; justify-content: center;
  background: url(/assets/ui/medallion_frame.png?v=5) center/100% 100% no-repeat;
}
.icon-frame img.icon, .icon-frame .ph, .icon-frame img.big { width: 66%; height: 66%; object-fit: contain; border: none; background: none; padding: 0; line-height: 1; box-shadow: none; }

/* ----- plain square frame for BUILDING portraits (user request: "remove
   background for all the buildings and put them in square frames" — the
   round medallion above was designed for unit art, which is already
   background-removed; building art historically had a painted scene baked
   into a square photo, which looked wrong inset into a round frame. Fix is
   two-part: (1) every building display now uses buildingSprite() — the
   already-existing background-removed /assets/cutout/buildings/ art (same
   pipeline used for the village-grid sprites) — instead of buildingArt()'s
   painted-background original, and (2) a plain bordered square box instead
   of the medallion.) ----- */
.icon-frame-sq {
  position: relative; flex-shrink: 0; display: inline-flex; align-items: center; justify-content: center;
  background: var(--surface-cont); border: 2px solid var(--outline); border-radius: 8px;
  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, .5), 0 1px 3px rgba(0, 0, 0, .18);
}
.icon-frame-sq img.icon, .icon-frame-sq .ph, .icon-frame-sq img.big { width: 74%; height: 74%; object-fit: contain; border: none; background: none; padding: 0; line-height: 1; box-shadow: none; }

/* ----- attack/health stat icons inline with text ----- */
.stat-ico { width: 17px; height: 17px; object-fit: contain; vertical-align: -4px; margin-right: 1px; }

/* ----- carved-plaque buttons (art has "COLLECT"/"UPGRADE" baked in) ----- */
.btn-plaque {
  display: inline-block; border: none; background-color: transparent; cursor: pointer;
  background-repeat: no-repeat; background-position: center; background-size: 100% 100%;
  transition: transform .1s, filter .15s;
}
.btn-plaque:hover { filter: brightness(1.08); transform: translateY(-1px); }
.btn-plaque:active { transform: translateY(1px); filter: brightness(.96); }
/* active/toggled state (e.g. Edit-layout mode is on) — the art itself can't
   change color, so indicate it with an outer gold ring instead. */
.btn-plaque.primary { box-shadow: 0 0 0 3px rgba(255, 214, 120, .8), 0 4px 14px rgba(0, 0, 0, .4); border-radius: 3px; }
.btn-plaque-collect { background-image: url(/assets/ui/btn_collect.png?v=5); width: 108px; height: 40px; }
.btn-plaque-upgrade { background-image: url(/assets/ui/btn_upgrade.png?v=5); width: 140px; height: 44px; }
/* carved-marble "TRAIN" plaque (Army page unit cards) — real art, no text baked in via CSS */
.btn-plaque-train { background-image: url(/assets/ui/btn_train.png?v=1); height: 44px; min-width: 76px; }
/* Bronze/marble "BUILD" and "EDIT LAYOUT" plaques (July 29 2026, user-supplied
   Stitch renders) — replace the Polis action rail's old plain-text buttons.
   Source art is ~900x358 and ~900x363 respectively; sized to match the rail's
   existing btn-plaque-collect/upgrade proportions.
   Round 30 follow-up 2 ("make edit layout, build and progress same size
   buttons"): all three action-rail nameplates below share one fixed size —
   their source art is all ~2.5:1 already, so stretching each into the same
   box causes no visible distortion. */
.btn-plaque-build, .btn-plaque-editlayout, .btn-plaque-progress { width: 112px; height: 44px; }
.btn-plaque-build { background-image: url(/assets/ui/btn_build.png?v=1); }
.btn-plaque-editlayout { background-image: url(/assets/ui/btn_edit_layout.png?v=1); }
/* Bronze/marble "PROGRESS" nameplate (round 30, user-supplied Stitch render,
   stitch_agon_strategy_game_ui-38) — opens the Building Progress modal (see
   showBuildProgress() in town.js). Source art is ~900x363 like the two
   plaques above. Originally docked to its own fixed bottom-left corner;
   round-30-follow-up moved it into `.town-actions-right` directly under
   Build (see town.js) per "put progress button right under build". */
.btn-plaque-progress { background-image: url(/assets/ui/btn_progress.png?v=1); }

/* ----- full stat readout chips (July 29 2026: now used directly inside each
   Train Troops unit-card, see `.unit-card-stats` above — used to be scoped under a
   `.unit-detail` wrapper that only ever existed inside the click-triggered side
   popup; the popup is gone, so this is general-purpose now.) ----- */
.stat-grid {
  display: grid; grid-template-columns: 1fr 1fr; gap: 8px 12px;
}
.stat-row {
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
  padding: 6px 10px; background: rgba(122, 99, 78, .08); border-radius: 4px;
  font-size: 13px; font-weight: 700; color: #3d2e1c;
}
.stat-row .stat-row-label { display: flex; align-items: center; gap: 5px; color: #7a634e; font-weight: 700; }
.stat-row .stat-row-label img.stat-ico { width: 15px; height: 15px; margin-right: 0; vertical-align: -3px; }

/* ----- progress bars: "filling vessel" ----- */
.vessel {
  height: 14px; border-radius: 7px; background: #3a3f45;
  border: 1px solid #262b30; overflow: hidden; position: relative;
}
.vessel .fill {
  height: 100%; border-radius: 6px;
  background: linear-gradient(90deg, #1c6390, #2d8fc7 60%, #90cdff);
  transition: width .5s linear;
}
.vessel.gold .fill { background: linear-gradient(90deg, #8a6508, #d4a017 70%, #ffe9a8); }

/* ----- input ----- */
input[type=text], input[type=password], input[type=number], select {
  font-family: var(--sans); font-size: 15px;
  background: var(--surface-cont);
  border: 1px solid var(--outline-var); border-radius: 4px;
  box-shadow: inset 0 2px 5px rgba(85, 67, 54, .18);
  padding: 8px 12px; color: var(--on-surface);
  outline: none;
}
input:focus, select:focus { border: 2px solid var(--primary-cont); padding: 7px 11px; }

/* ----- grid cards ----- */
/* Centered (Aug 6 2026 — "Make all the items centered and when there are few
   of them - to be seen on top"): auto-fill with 1fr columns stretched a lone
   card across the whole row and left the rest as a ragged first line. Fixed
   column width + justify-content centers however many cards there are, and a
   grid always fills from the top row down. */
.cards {
  /* 248, not 230 (Aug 8 2026): the wooden frame takes 38px of every card, and
     at 230 the longest price in the shop ended up two pixels from a carved
     cap. Widening the column buys that back for the whole card, not just the
     price — the pilasters read better with the extra room too. */
  display: grid; grid-template-columns: repeat(auto-fill, 248px);
  justify-content: center; align-content: start; gap: 16px;
}
.card { padding: 14px; text-align: center; }
/* `margin-inline: auto` is the whole fix for a bug that outlived four rounds
   of "the images are not centred" (Aug 7 2026). Every previous attempt
   re-cut the ARTWORK — measuring the opaque pixels inside the PNG, which
   were centred to within 0.3% the entire time. The circle itself was the
   thing off-centre: this is a BLOCK-level element, so the card's
   `text-align: center` never touched it, and it sat flush against the card's
   left padding edge — 54px left of centre on a 230px card. Nothing about the
   art could ever have fixed that. */
.card img.big {
  width: 92px; height: 92px; border-radius: 50%; object-fit: contain;
  border: 3px solid var(--outline-var); background: var(--surface-cont); padding: 4px;
  display: block; margin-inline: auto;
}
.card.legend img.big { border-color: var(--gold-bright); box-shadow: 0 0 14px rgba(212,160,23,.5); }

/* ----- battle ----- */
.battle-wrap { display: grid; grid-template-columns: 1fr 300px; gap: 18px; align-items: start; }
@media (max-width: 1000px) { .battle-wrap { grid-template-columns: 1fr; } }
#battle-canvas {
  width: 100%; border-radius: 8px; border: 2px solid var(--outline);
  background: linear-gradient(180deg, #c8b98e, #b3a276);
  box-shadow: 0 10px 28px rgba(27,28,21,.3);
  display: block; touch-action: manipulation;
}
.init-bar { display: flex; gap: 6px; flex-wrap: wrap; margin-bottom: 10px; }
.init-chip {
  display: flex; align-items: center; gap: 5px; padding: 2px 8px 2px 2px;
  border-radius: 999px; font-size: 12px; font-weight: 700;
  border: 1px solid var(--outline-var); background: var(--surface-cont);
}
.init-chip img { width: 22px; height: 22px; border-radius: 50%; object-fit: cover; }
.init-chip.att { border-color: var(--secondary); background: #e2f1fd; }
.init-chip.def { border-color: var(--tertiary); background: #ffe7da; }
.init-chip.current { box-shadow: 0 0 0 2px var(--gold-bright); }
.battle-log {
  max-height: 300px; overflow-y: auto; font-size: 13px;
  display: flex; flex-direction: column-reverse;
}
.battle-log div { padding: 2px 0; border-bottom: 1px dotted var(--outline-var); }
.favor-bar { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 10px; }

/* build tray (July 29 2026 — moved from a horizontal bottom bar to a fixed
   vertical rail on the LEFT edge of the Polis screen, cards top-to-bottom,
   mirroring the existing `.town-actions-right` fixed vertical rail on the
   opposite edge — "make buildings on the left side from up to down"). */
.build-tray {
  display: none; /* toggled via the `.open` class from town.js, NOT an inline
    style.display — see the comment above the #btn-catalog handler in town.js
    for why: an inline `display:block` would beat this rule's `display:flex`
    below and silently break the vertical layout / overflow clipping. */
  position: fixed; top: 50%; left: 24px; transform: translateY(-50%);
  width: 172px; max-height: 82vh; z-index: 25;
  background: linear-gradient(180deg, #f8f2e2, #eadcbe);
  border: 2px solid #7a5a3a; border-radius: 10px;
  box-shadow: inset 0 0 0 1.5px rgba(212,160,23,.45), 0 10px 26px rgba(30,20,10,.45);
  padding: 8px 10px 10px;
  flex-direction: column; min-height: 0;
  overflow: hidden; /* belt-and-suspenders: the tray's own rounded border box
    never lets a long card list visually spill past it, even if the flex-sizing
    below somehow fails to clip #tray-cards first. */
}
.build-tray.open { display: flex; }
.build-tray .tray-title {
  text-align: center; font-weight: 800; font-size: 12px; letter-spacing: .1em;
  color: #5d4630; margin-bottom: 6px; flex-shrink: 0;
}
/* `flex: 1` (flex-basis: 0, not the default `auto`) is the fix for a real bug
   (July 29 2026 — "buiolding list doesnt go out from the borders"): with a
   long unlocked-building list (many buildings unlock by Acropolis level 10),
   a flex-basis of `auto` sizes this to its full content height regardless of
   the parent's `max-height`, so cards rendered past the tray's own border —
   `flex:1` + `min-height:0` forces it to actually shrink to the space left
   inside the tray, so `overflow-y:auto` has something real to clip/scroll. */
.tray-cards { flex: 1; display: flex; flex-direction: column; gap: 8px; overflow-y: auto; overflow-x: hidden; padding-right: 2px; min-height: 0; }
.tray-card {
  flex: 0 0 auto; width: 100%;
  display: flex; flex-direction: column; align-items: center; gap: 3px;
  background: linear-gradient(180deg, #fbf6e8, #f0e3c6);
  border: 1.5px solid #8a6a48; border-radius: 8px; padding: 7px 4px 5px;
  cursor: pointer; font-family: var(--sans);
  box-shadow: 0 2px 3px rgba(60,40,20,.22);
  transition: transform .1s, box-shadow .1s;
}
.tray-card:hover { transform: translateY(-2px); box-shadow: 0 0 0 2px var(--gold-bright), 0 5px 9px rgba(60,40,20,.3); }
.tray-card img, .tray-card .ph {
  width: 52px; height: 52px; object-fit: contain;
  display: flex; align-items: center; justify-content: center; font-size: 32px;
}
.tray-card b { font-size: 10.5px; color: #3d2e1c; line-height: 1.15; text-align: center; }
/* Round 30: a card can now list 2+ resource chips (most buildings cost more
   than just Argyrion now — see gamedata.js) — wrap onto a second line
   instead of overflowing the card. */
.tray-card .tray-cost { display: flex; align-items: center; justify-content: center; flex-wrap: wrap; gap: 3px 6px; font-size: 11px; font-weight: 700; color: #6b5138; }
.tray-card .tray-cost span { display: flex; align-items: center; gap: 2px; }
.tray-card .tray-cost img { width: 14px; height: 14px; object-fit: contain; }
/* Round 30-follow-up-11 — a not-yet-unlocked building now stays in the tray
   (dimmed, with a lock badge over its art and the reason in place of the cost
   chips) instead of just disappearing. Click still gives feedback via a
   toast rather than silently doing nothing. */
.tray-art-wrap { position: relative; display: flex; align-items: center; justify-content: center; }
.tray-card.locked { cursor: default; }
.tray-card.locked:hover { transform: none; box-shadow: 0 2px 3px rgba(60,40,20,.22); }
.tray-card.locked img, .tray-card.locked .ph { filter: grayscale(85%) brightness(.75); opacity: .65; }
.tray-card.locked b { opacity: .6; }
.tray-lock {
  position: absolute; right: -4px; bottom: -4px; font-size: 16px;
  background: rgba(30,22,12,.85); border-radius: 50%; width: 22px; height: 22px;
  display: flex; align-items: center; justify-content: center;
  box-shadow: 0 1px 2px rgba(0,0,0,.4);
}
.tray-lock-reason { font-size: 10px; font-weight: 600; color: #8a6a48; text-align: center; line-height: 1.2; }

/* war map */
.war-map {
  position: relative; border-radius: 10px; overflow: hidden;
  min-height: 420px; border: 2px solid var(--outline);
  background:
    radial-gradient(ellipse 60% 40% at 70% 20%, rgba(255,255,255,.35), transparent 60%),
    linear-gradient(160deg, #d8cba3 0%, #c9b98c 40%, #a9c4b2 75%, #7db3cc 100%);
}
.war-node {
  position: absolute; transform: translate(-50%,-50%);
  display: flex; flex-direction: column; align-items: center; gap: 4px;
  background: none; border: none; cursor: pointer; z-index: 2;
}
.war-node .disc {
  width: 74px; height: 74px; border-radius: 50%;
  display: flex; align-items: center; justify-content: center; font-size: 30px;
  border: 3px solid var(--outline); background: var(--surface-low);
  box-shadow: 0 5px 14px rgba(27,28,21,.35);
  transition: transform .12s;
}
.war-node:hover .disc { transform: scale(1.07); }
.war-node.mine .disc { border-color: var(--secondary); box-shadow: 0 0 0 3px rgba(28,99,144,.4), 0 5px 14px rgba(27,28,21,.35); }
.war-node.theirs .disc { border-color: var(--error); box-shadow: 0 0 0 3px rgba(186,26,26,.35), 0 5px 14px rgba(27,28,21,.35); }

/* misc */
.grid2 { display: grid; grid-template-columns: 1fr 1fr; gap: 18px; }
@media (max-width: 900px) { .grid2 { grid-template-columns: 1fr; } }

/* World hub landing page (July 29 2026, then bumped bigger + stripped to just
   the icons: "make the icons in raid even bigger and leave only them") — Raid
   stays on the left, Campaign on the right (document order = flex order); no
   page title, no card background/text, just two big clickable scroll icons
   centered in the available viewport height. */
.world-choice {
  display: flex; justify-content: center; align-items: center;
  gap: clamp(40px, 8vw, 140px);
  min-height: calc(100vh - 132px); /* clears the floating topbar, see body.has-page-bg .main padding-top */
}
.world-choice-card {
  display: flex; flex-direction: column; align-items: center; gap: 10px;
  text-decoration: none; cursor: pointer;
}
.world-choice-card .wc-icon {
  width: clamp(220px, 26vw, 420px); height: auto; object-fit: contain;
  /* no drop-shadow — a "light behind" glow/shadow effect here wasn't reading
     right, so hover is now JUST a plain scale-up, nothing else. transform-
     origin defaults to center, so the image grows outward evenly in all
     directions — the cursor, already inside the smaller box, stays inside
     the larger one, so hover can't self-cancel the way translateY did. */
  transform: scale(1);
  transition: transform .18s ease-out;
}
.world-choice-card:hover .wc-icon { transform: scale(1.08); }
/* Chiseled-stone "RAID"/"ODYSSEY" title plaques, stacked above each icon
   (normal flow, not overlaid) — pointer-events:none so they don't interfere
   with the hover/click target, which stays the whole card. Not scaled on
   hover (only the icon underneath is) so the title reads as a fixed
   nameplate. FIXED height (not width-relative) is what actually makes the
   two icons land "on the same level" (round 22) — titleRaid and
   titleOdyssey have different source aspect ratios, so sizing both by width
   gave them different heights, which pushed the icons below them to
   different vertical offsets inside the centered flex row. */
.world-choice-card .wc-title {
  width: auto; max-width: 80%; height: clamp(46px, 6vw, 88px);
  object-fit: contain; pointer-events: none;
  filter: drop-shadow(0 6px 14px rgba(0, 0, 0, .6));
}
@media (max-width: 700px) { .world-choice { flex-direction: column; gap: 24px; min-height: auto; padding: 40px 0; } }

/* Raid sub-page (July 30 2026 — "dont make raid page scrollable, it should
   be fixed for now"; still scrollable after a first attempt that sized
   `.raid-page` via `calc(100vh - 132px - 108px)` against `.main`'s padded
   box — that padding-based math was too fragile against the topbar's real
   rendered height). Round 26 fix: `#/raid` now gets `main-flush` (see
   app.js's FLUSH_ROUTES) same as Town/Campaign, so `.raid-page` is a DIRECT
   flex:1;min-height:0 child of that flush `<main>` — the one relationship
   already proven (twice) to reliably fill exactly the viewport, no vh/px
   guessing involved. `main-flush` strips `.main`'s own padding to 0, so
   `.raid-page` restores it directly (132px top clears the floating topbar,
   108px bottom clears the fixed bottom-nav, same numbers as before — just
   applied one level lower where they can't drift out of sync with the real
   box). The opponent list and battle-report list each scroll inside their
   own panel instead of the whole page scrolling. */
.raid-page {
  flex: 1; min-height: 0; display: flex; flex-direction: column; gap: 16px;
  padding: 132px 32px 108px; box-sizing: border-box; overflow: hidden;
  position: relative;
  /* Paints its own backdrop now (round 26) instead of going through
     body.has-page-bg (see the PAGE_BGS comment in assets.js for why). Kept
     as a plain image (round 30-follow-up-19 — "keep the background picture
     for raid, but add on top of it" the looping video below) — this is now
     just the pre-video-load paint / no-video fallback, not the visible
     backdrop in the common case. */
  /* Aug 7 2026 — paints NOTHING. The scene now comes from the shared
     body-level backdrop (.page-bg, below), the same one the world hub uses,
     so the two pages cannot show different framings of the same picture. */
  background: transparent;
}
/* The RAID button and the report list sit above the shared backdrop; nothing
   in this page paints a scene of its own any more. */
.raid-page > * { position: relative; z-index: 2; }
.raid-page .raid-opps { max-height: 220px; overflow-y: auto; }
/* Aug 4 2026 — "add moving background same as in raid. Leave only big button
   and history at the bottom." The looping video was always there; two opaque
   panels were sitting on top of it. Now the button floats on the video and the
   history is a translucent strip pinned to the bottom, so the motion is the
   page rather than a border around it. */
.raid-page .raid-stage {
  flex: 1; min-height: 0;
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 14px;
}
.raid-page .raid-big-btn-sub {
  color: #f3e9d6; text-shadow: 0 2px 6px rgba(0, 0, 0, .75);
  font-size: .95rem; letter-spacing: .03em; text-align: center;
}
/* Aug 9 2026 — "the battle reports is still not a scroll": the glass card
   gave way to the parchment scroll itself. Dispatches from the field, on the
   document they belong on. */
.raid-page .raid-history {
  flex: none; max-height: 44vh; min-height: 150px;
  display: flex; flex-direction: column; gap: 8px;
  border: solid transparent;
  border-width: 58px 50px 60px 50px;
  border-image: url('/assets/ui/scroll_frame.png?v=38') 175 160 175 160 fill / 58px 50px 60px 50px stretch;
  border-radius: 0; background: none;
  margin: -6px 0;
  /* Same lesson the Koinon scrolls taught: the torn parchment edge wanders
     further in than the border width, so the frame alone never guarantees a
     margin — this padding does ("sides are very close to borders"). */
  padding: 0 34px;
}
.raid-history-title {
  margin: -10px 0 0; font-size: 1.15rem; color: #2a2013; text-shadow: none;
}
/* ink and chips re-tuned for parchment instead of dark glass */
.raid-page .raid-history { color: #2a2013; --backdrop: 232, 220, 194; }
.raid-page .raid-history .raid-report {
  background: rgba(90, 66, 34, .08);
  border-color: rgba(90, 66, 34, .3); color: #2a2013;
}
.raid-page .raid-history .raid-report b { color: #1d1408; }
.raid-page .raid-history .raid-report .muted,
.raid-page .raid-history .raid-report-age { color: #6d5c42; }
.raid-page .raid-history .raid-reports > .muted { color: #6d5c42; }
.raid-page .raid-history .raid-chip {
  background: rgba(255, 251, 240, .65); border-color: rgba(90, 66, 34, .35); color: #43301a;
}
.raid-page .raid-history .raid-chip-loot { background: rgba(212, 160, 23, .25); border-color: var(--gold); color: #5c430e; }
.raid-page .raid-history .raid-chip-up { background: rgba(92, 138, 58, .22); border-color: #5c8a3a; color: #3d6127; }
.raid-page .raid-history .raid-chip-down { background: rgba(186, 26, 26, .14); border-color: var(--error); color: #8c1616; }
.raid-page .raid-reports { flex: 1; overflow-y: auto; min-height: 0; }

/* Odyssey campaign map + its per-stop sub-maps like Troy (July 30 2026 —
   "make so the map is on the whole screen and each number on the map is
   clickable", then round 26: "make odyssey map fullscreen, for now there
   are black sidebars" + "find map that will open (also fullscreen)"):
   full-bleed like the Polis canvas (.town-root/.town-canvas-wrap below)
   instead of a boxed panel — see renderCampaign()/renderTroyMap() in
   world.js, which render this as the page's only content with main in
   main-flush mode. `.odyssey-map-frame` is reused for BOTH maps (both are
   1200×896 art). */
.odyssey-map-page {
  position: relative; flex: 1; min-height: 0;
  background: #14100a; overflow: hidden;
}
/* Sized to COVER the page (round 26 — no black letterbox bars), not contain
   it: `min-width:100%;min-height:100%` (rather than max-width/max-height)
   means the browser grows the box until BOTH dimensions satisfy their floor,
   picking whichever axis needs to grow more and deriving the other from
   `aspect-ratio` — same mechanism as `object-fit:cover`, just on a generic
   box instead of an <img>, so it still keeps a single element whose OWN
   box is exactly the art's 1200:896 shape (letting hotspot %s line up)
   while the excess on one axis gets cropped by the parent's overflow:hidden
   instead of padded out to black. Absolutely positioned + centered (not a
   flex child) because flexbox's main/cross sizing pass can't reliably
   resolve a `height:100%`+`aspect-ratio` combo on a flex item — it's a
   circular dependency that made an earlier attempt stick at the image's
   native 1200×896 forever, regardless of screen size. */
.odyssey-map-frame {
  position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
  min-width: 100%; min-height: 100%; width: auto; height: auto;
  aspect-ratio: 1200 / 896;
}
/* Scoped to .odyssey-map-img specifically (NOT a bare `.odyssey-map-frame
   img` descendant selector) — the numbered badges below are also <img>
   children of this same frame, and a generic `img` selector here was
   stretching every badge to the frame's full size instead of its own 6.4%
   (a real round-26 bug: badge bounding rects all came back matching the
   frame's rect until this got scoped). */
.odyssey-map-frame .odyssey-map-img { display: block; width: 100%; height: 100%; object-fit: cover; }
/* Clickable numbered badges (round 26 — carved bronze/marble plaques,
   replacing the earlier invisible discs) laid over each stop; see
   ODYSSEY_STOPS/TROY_STOPS in world.js for the per-map coordinate lists.
   "Bigger when you point on them" is a plain hover scale, same
   transform-origin-center trick as `.wc-icon` so the cursor never falls
   outside the grown box.
   Round 30-follow-up-9 went through 3 passes: (1) a floating title pill
   above a still-big badge; (2) name-primary with the badge shrunk to a
   small 30px accessory underneath; (3, current — "remove text on top of
   the number and make all exactly 3 times bigger") the visible name label
   is gone again (back to hover-tooltip-only via the `title` attr, same as
   every round before pass 1) and `.odyssey-badge-wrap` is back to being
   just the badge box itself — no more flex/column/title layout to reason
   about, only its size changed (3× pass 2's fixed box). */
.odyssey-badge-wrap {
  position: absolute; transform: translate(-50%, -50%) scale(1);
  width: 3.5%; aspect-ratio: 1; cursor: pointer; transition: transform .15s ease-out;
  z-index: 1;
}
/* Round 30-follow-up-12 ("make the numbers smaller and adjust them to where
   the background numbers are" — Troy sub-map only, per the user). Measuring
   the actual printed circles on troy_map.jpg (cv2.HoughCircles on a tight
   crop around each) put their radius at ~1.12% of frame width — i.e. about
   2.2% across — while the shared badge above is 3.5%, noticeably bigger than
   the circle it's meant to sit on. Scoped to `.troy-page` so the Odyssey
   overview map (not part of this request) keeps its existing 3.5%.
   Round 30-follow-up-20 ("keep the size of red on the photo"): the user's
   annotated red dots (measured the same way — connectedComponentsWithStats
   on the flat-red mask, median ~39px across a 1200px-wide frame) are
   noticeably bigger than the 2.2% circles this size was originally tuned to.
   Bumped to match the dot size itself, since that's now the explicit target
   rather than the map's own printed rings. */
/* The Odyssey map, behind the Troy sheet (Aug 13 2026). Covers the page the
   same way .odyssey-map-frame does, but WITHOUT the aspect-ratio box: this one
   has no hotspots to line up, so plain object-fit:cover is enough and it can
   simply fill whatever shape the window is.

   The dim and the blur are what stop it competing. Left at full strength the
   two maps read as one busy surface and the Troy sheet stops being the thing
   you are looking at; at this weight it reads as the table the sheet is lying
   on. z-index 0 against the frame's own stacking keeps it under both the map
   and its badges. */
.troy-backdrop {
  position: absolute; inset: 0; z-index: 0;
  width: 100%; height: 100%; object-fit: cover;
  filter: brightness(.42) saturate(.75) blur(2px);
  /* The blur samples past the edges and would otherwise show a soft pale rim
     where it runs out of picture. */
  transform: scale(1.03);
  pointer-events: none;
}
/* Aug 13 2026 — "make the map bigger."
   The frame already grows until it COVERS the viewport, so the sheet was as
   large as cover-sizing can make it; what was left over was the parchment's
   own inset inside its bounding box — the tear sits ~1.4% in from the image
   edge, which put the torn edge about 21px inside the screen on each side and
   left a band of backdrop (and, before the re-cut, a white hairline) on show.
   Scaling past cover carries that tear off the edge of the screen, so the map
   runs to the sides and the Odyssey behind it now shows only through the deep
   bays of the tear, which is a better place for it anyway.
   The badges are positioned in per-cent of this frame, so they scale with it
   and every hotspot stays exactly where its landmark is. */
.troy-page .odyssey-map-frame { z-index: 1; }
/* The boost is conditional, and the condition is not cosmetic. Cover-sizing
   already crops hard at extreme window shapes — on a 2560×1080 ultrawide the
   frame stands 1911px tall inside a 1080px window — and scaling past cover
   crops further. Measured across seven window shapes: at every ordinary
   desktop and laptop proportion all eight hotspots stay on screen at 1.1, but
   on an ultrawide it carried the lowest badge off the bottom and in portrait
   it carried two off the sides, which does not just look wrong, it makes those
   beats of the campaign unreachable. So the boost applies where there is room
   for it and stands down where there is not. */
@media (min-aspect-ratio: 1/1) and (max-aspect-ratio: 2/1) {
  .troy-page .odyssey-map-frame { transform: translate(-50%, -50%) scale(1.1); }
}
/* The sheet needs a shadow now that it is demonstrably lying on something —
   without it the cut-out edge reads as a rendering fault rather than as paper.
   drop-shadow rather than box-shadow: the element is a torn silhouette, and
   box-shadow would trace its rectangular box instead of the parchment. */
.troy-page .odyssey-map-img { filter: drop-shadow(0 10px 26px rgba(0, 0, 0, .7)); }

.troy-page .odyssey-badge-wrap { width: 3.25%; }
/* At 90px, close-together stops genuinely overlap (confirmed + accepted by
   the user — "keep exactly 3x, overlap and all"). But that overlap made the
   CURRENTLY UNLOCKED stop (the one actually meant to be clicked) end up
   completely buried under a neighboring locked badge in DOM/paint order —
   not just visually messy, actually unclickable, which breaks the one
   interaction that has to keep working. This z-index bump is the minimal
   fix: whichever stop is unlocked always paints on top of any locked
   neighbor, so it's reachable no matter how the circles overlap. Locked
   stops overlapping each other is left as-is per the user's call — they
   only ever shake, so which one you land on among several piled-up locked
   badges doesn't affect anything. */
.odyssey-badge-wrap:not(.locked) { z-index: 5; }
.odyssey-badge-wrap:hover, .odyssey-badge-wrap:focus-visible {
  transform: translate(-50%, -50%) scale(1.1);
  z-index: 10;
  outline: none;
}
/* The plaque+lock box. Round 30-follow-up-9 tried 30px (pass 2, a minor
   accessory under a name label) and 90px fixed (pass 3, "exactly 3 times
   bigger" — but that overlapped badly since stops sit as close as ~4% of
   frame width apart, confirmed with the user and then walked back: "make
   them just right to fit in the circle where they were"). Landed back on
   %-of-frame sizing (not a fixed px) — the same 4.2% every earlier round
   (26 through today) converged on before any of this round's experiments,
   which is exactly "where they were": big enough to read clearly, small
   enough that no two stops' plaques overlap at any real viewport width.
   Needs its own `position: relative` so `.odyssey-lock-icon` centers on
   just the plaque. */
.odyssey-badge-img-wrap { position: relative; width: 100%; height: 100%; }
.odyssey-badge {
  display: block; width: 100%; height: 100%; object-fit: contain;
  filter: drop-shadow(0 3px 8px rgba(0, 0, 0, .55));
  transition: filter .15s ease-out;
}
.odyssey-badge-wrap:hover .odyssey-badge, .odyssey-badge-wrap:focus-visible .odyssey-badge {
  filter: drop-shadow(0 3px 8px rgba(0, 0, 0, .55)) drop-shadow(0 0 10px rgba(212, 160, 23, .85));
}
/* Locked stops (round 28 — see unlockedIdSet()/stopBadgesHtml() in
   world.js): desaturated + dimmed at rest. Round 28's color-only fix for
   "the plaque reads like a 9" wasn't enough (round 30 user report, with a
   screenshot, showed it STILL reads as "9" repeated everywhere even
   dimmed) — the shackle-and-loop SHAPE carved into the plaque itself is the
   problem, not its color, and no filter changes a shape. Real fix: an
   unambiguous closed-padlock SVG glyph (`.odyssey-lock-icon`, built in
   stopBadgesHtml()) drawn centered on top of the plaque, dark body + light
   stroke so it reads clearly against any of the plaque's carved detail
   underneath. The dimming stays too, as a secondary "muted, not a real
   number" cue. Hover stays duller (no gold glow — reserved for enterable
   stops) plus a "no" shake instead of navigating on click. */
.odyssey-badge-wrap.locked .odyssey-badge {
  filter: drop-shadow(0 3px 8px rgba(0, 0, 0, .55)) grayscale(70%) brightness(.72);
}
.odyssey-badge-wrap.locked:hover, .odyssey-badge-wrap.locked:focus-visible {
  transform: translate(-50%, -50%) scale(1.04);
}
.odyssey-badge-wrap.locked:hover .odyssey-badge, .odyssey-badge-wrap.locked:focus-visible .odyssey-badge {
  filter: drop-shadow(0 3px 8px rgba(0, 0, 0, .55)) grayscale(70%) brightness(.72);
}
.odyssey-lock-icon {
  position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
  width: 52%; height: 52%; pointer-events: none;
  filter: drop-shadow(0 1px 3px rgba(0, 0, 0, .8));
}
@keyframes odyssey-badge-shake {
  10%, 90% { transform: translate(-50%, -50%) translateX(-1px); }
  20%, 80% { transform: translate(-50%, -50%) translateX(2px); }
  30%, 50%, 70% { transform: translate(-50%, -50%) translateX(-5px); }
  40%, 60% { transform: translate(-50%, -50%) translateX(5px); }
}
.odyssey-badge-wrap.shake { animation: odyssey-badge-shake .5s ease-in-out; }
/* Back / legacy-missions controls dock to fixed screen corners — same
   pattern as .signout-corner/.town-actions-right — so they hold their spot
   regardless of the map frame's own size, clear of the topbar above and the
   nav dock centered at the bottom. */
.odyssey-back { position: fixed; left: 24px; bottom: 24px; z-index: 25; }
.odyssey-missions-btn { position: fixed; right: 24px; bottom: 24px; z-index: 25; }

.mt { margin-top: 16px; } .mb { margin-bottom: 16px; }
.center { text-align: center; }
.toast {
  position: fixed; bottom: 24px; left: 50%; transform: translateX(-50%);
  background: var(--inverse-surface); color: var(--inverse-on-surface);
  padding: 10px 22px; border-radius: 6px; z-index: 300; font-weight: 600;
  border: 1px solid var(--gold); box-shadow: 0 8px 24px rgba(0,0,0,.4);
  animation: rise .2s ease-out;
}
.toast.err { border-color: var(--error); }
.shield-banner {
  display: flex; align-items: center; gap: 10px;
  background: linear-gradient(90deg, rgba(144,205,255,.35), transparent);
  border: 1px solid var(--secondary-cont); border-radius: 6px;
  padding: 8px 14px; font-weight: 600;
}

/* Aug 11 2026 — the laurel on its column at the head of the Koinon hall, the
   same render that heads the Koinon War page. Sized here rather than left at
   the art's own 512px. */
.k-hall-ico { width: 68px; height: 68px; object-fit: contain; flex: none; }

/* Aug 11 2026 — the Recruitment Shield as a corner badge rather than a banner
   across the top of the polis. The clock stays visible because it is the part
   worth glancing at; the explanation is behind a press. */
.shield-badge { position: relative; display: inline-block; }
.shield-badge-btn {
  display: inline-flex; align-items: center; gap: 7px;
  padding: 5px 12px 5px 6px; cursor: pointer;
  background: rgba(20, 15, 8, .72);
  border: 1px solid rgba(144, 205, 255, .55); border-radius: 999px;
  box-shadow: 0 4px 14px rgba(0, 0, 0, .4);
  transition: border-color .2s, background .2s, transform .2s;
}
.shield-badge-btn:hover { background: rgba(20, 15, 8, .88); border-color: var(--secondary-cont); transform: translateY(-1px); }
.shield-badge-btn img { width: 30px; height: 30px; object-fit: contain; display: block; }
.shield-badge-clock {
  font-size: 13px; font-weight: 800; color: #d6ebff; letter-spacing: .02em;
  font-variant-numeric: tabular-nums;
}
.shield-note {
  position: absolute; top: calc(100% + 8px); left: 0; z-index: 40;
  width: min(330px, 78vw); padding: 14px 16px;
  background: rgba(20, 15, 8, .96);
  border: 1px solid rgba(144, 205, 255, .5); border-radius: 8px;
  box-shadow: 0 14px 36px rgba(0, 0, 0, .55);
  font-size: 13px; line-height: 1.55; color: #efe4c8;
  animation: rise .18s ease-out;
}
.shield-note[hidden] { display: none; }
.shield-note b { color: #d6ebff; display: block; margin-bottom: 6px; font-size: 14px; }
.shield-note p { margin: 0 0 8px; }
.shield-note p:last-child { margin-bottom: 0; }
.shield-note .muted { color: #b9ab8e; }
.shield-note .muted b { display: inline; color: #efe4c8; font-size: 13px; }
.auth-hero {
  min-height: 100vh; display: flex; align-items: center; justify-content: center;
  background:
    radial-gradient(ellipse 90% 55% at 50% -8%, #fff3d6 0%, transparent 55%),
    linear-gradient(180deg, #aed4e8 0%, #cfe6ef 30%, #7db3cc 45%, #d5c9a1 60%, #a9986e 100%);
  padding: 20px;
}
.auth-card { max-width: 420px; width: 100%; }
.auth-card img.hero-logo {
  width: 110px; height: 110px; border-radius: 50%; object-fit: cover;
  border: 4px solid var(--gold-bright); box-shadow: 0 10px 30px rgba(27,28,21,.35);
}
.tabbtns { display: flex; gap: 6px; margin-bottom: 14px; flex-wrap: wrap; }
.tabbtns .btn.on { box-shadow: inset 0 0 0 2px var(--gold-bright); background: var(--primary-fixed); }
table.plain { width: 100%; border-collapse: collapse; font-size: 14px; }
table.plain th { text-align: left; font-size: 12px; letter-spacing: .08em; text-transform: uppercase; color: var(--on-surface-var); padding: 6px 8px; }
table.plain td { padding: 7px 8px; border-top: 1px dashed var(--gold); }

/* ----- Koinon Request/Donate frame banners (July 29 2026) — the two
   temple-pediment stone frames the user pointed to in the project folder.
   Each panel's banner sits at the LEFT of its own header, title text to the
   right, per "make so the one for attack is on the left ... flip
   horizontally the one for defence and make it on the left" — attack (crossed
   spears + gorgon shield, requesting troops) stays as-is, defence (fortress
   relief, donating troops) is mirrored via scaleX(-1) but positioned the same
   way at the left of its own panel. */
/* League crests (Aug 8 2026) — the user's ten Bronze/Silver/Gold discs. */
.league-crest { width: 38px; height: 38px; object-fit: contain; flex: 0 0 auto; vertical-align: middle; }
.league-crest.sm { width: 24px; height: 24px; }
.rc-crest { width: 30px; height: 30px; object-fit: contain; filter: drop-shadow(0 1px 2px rgba(0,0,0,.35)); }
.account-league { display: flex; align-items: center; gap: 8px; }
.k-glory { display: flex; align-items: center; gap: 8px; }

/* ----- Koinon: two scrolls, side by side (Aug 8 2026) -----
   "In Kainon make 2 scrolls on the same page. left side is Kainon dashboard
   and rightside and leadership." The tabbed pair became a spread: the hall on
   the left, the week's leaders on the right, both on the same parchment.
   They stack under 1180px, where two framed scrolls would leave the narrower
   one with more border than content. */
.k-columns {
  display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 520px);
  gap: 10px; align-items: start; max-width: 1720px; margin: 0 auto;
}
.k-col { min-width: 0; }
@media (max-width: 1180px) {
  .k-columns { grid-template-columns: minmax(0, 1fr); gap: 0; }
}

.k-tabs { display: flex; gap: 8px; justify-content: center; margin-bottom: 14px; }
/* Both screens are drawn ON the parchment scroll, the same 9-slice the modals
   use — the page reads as a document rather than a stack of cards. */
.k-scroll {
  border: solid transparent;
  border-width: 128px 118px 130px 118px;
  border-image: url('/assets/ui/scroll_frame.png?v=38') 175 160 175 160 fill / 128px 118px 130px 118px stretch;
  max-width: 1080px; margin: 0 auto;
}
/* The side scroll carries the same frame at two-thirds scale. At full size its
   118px shoulders would eat most of a 520px column and leave the standings in
   a gutter. */
.k-scroll.k-scroll-sm {
  border-width: 88px 74px 90px 74px;
  border-image-width: 88px 74px 90px 74px;
  max-width: 560px;
}
/* Aug 8 2026 — "in Kainon both scrolls text is too close to the sides". The
   torn parchment edge wanders further in than the border width does, so the
   frame alone never guaranteed a margin; this does. */
.k-scroll-inner { color: #2a2013; --backdrop: 232, 220, 194; padding: 0 30px; }
.k-scroll-sm .k-scroll-inner { padding: 0 22px; }
@media (max-width: 720px) {
  .k-scroll-inner, .k-scroll-sm .k-scroll-inner { padding: 0 14px; }
}
.k-scroll-inner .panel { background: none; border: none; box-shadow: none; padding: 0 0 18px; }
.k-scroll-inner .panel-corner::before, .k-scroll-inner .panel-corner::after { display: none; }
.k-scroll-inner .muted { color: #6d5a3e; }
.k-scroll-inner h2, .k-scroll-inner h3 { color: #2a2013; }
/* Bright gold is a dark-panel colour; on parchment it measured 1.75:1. */
.k-scroll-inner .zone-hint, .k-scroll-inner .label-caps { color: #7a520f; opacity: 1; }
.k-scroll-inner .row { border-color: rgba(122, 90, 58, .3); }
@media (max-width: 720px) {
  .k-scroll { border-width: 74px 40px 76px 40px; border-image-width: 74px 40px 76px 40px; }
  .k-scroll.k-scroll-sm { border-width: 68px 38px 70px 38px; border-image-width: 68px 38px 70px 38px; }
}

/* ----- The week's standings ----- */
.k-lead-title { margin: 0 0 6px; }
.k-week {
  display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap;
  padding-bottom: 10px; border-bottom: 2px solid rgba(122, 90, 58, .35);
}
.k-week .label-caps { color: #6b4d12; }
/* The line that makes the Monday-to-Sunday cycle legible without a manual. */
.k-week-reset {
  margin: 10px 0 18px; padding: 8px 12px; border-radius: 6px;
  background: rgba(168, 118, 42, .14); border: 1px solid rgba(122, 90, 58, .3);
  font-size: 14px; color: #4a3a24;
}
.k-week-reset b { color: #6b4d12; }

.k-boards { display: grid; grid-template-columns: 1fr 1fr; gap: 26px; }
@media (max-width: 760px) { .k-boards { grid-template-columns: 1fr; } }
.k-board + .k-board { margin-top: 22px; }
.k-board-head { margin: 0 0 6px; }
.k-board .k-lb { list-style: none; margin: 0; padding: 0; }
.k-lb-row {
  display: flex; align-items: center; gap: 10px;
  padding: 7px 0; border-bottom: 1px dashed rgba(122, 90, 58, .3);
}
.k-lb-row:last-child { border-bottom: none; }
.k-lb-rank { flex: 0 0 24px; font-weight: 800; color: #75542c; text-align: center; }
.k-lb-top .k-lb-rank { color: #a8762a; font-size: 17px; }
.k-lb-crest { flex: 0 0 auto; }
.k-lb-name { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.k-lb-wins { flex: 0 0 auto; white-space: nowrap; color: #6b4d12; }
.k-lb-empty { font-size: 14px; }

/* The reader's own line, set apart from the ten above it. */
.k-standing { border-top: 2px solid rgba(122, 90, 58, .35); padding-top: 12px; }
.k-lb-me { background: rgba(168, 118, 42, .13); border-radius: 6px; padding: 7px 8px; }
.k-lb-me + .k-lb-me { margin-top: 5px; }

/* Last week, frozen — folded away so it never competes with the live board. */
.k-archive { margin-top: 22px; border-top: 2px solid rgba(122, 90, 58, .35); padding-top: 12px; }
.k-archive > summary { cursor: pointer; font-weight: 700; color: #6b4d12; list-style: revert; }
.k-archive .k-board { margin-top: 12px; }
.k-archive .k-lb-row { font-size: 14px; padding: 5px 0; }

/* A price, written in resources rather than words. */
.k-cost { display: inline-flex; align-items: center; gap: 4px; font-weight: 700; margin-right: 10px; }
.k-cost img { width: 18px; height: 18px; object-fit: contain; }
.k-fee { margin-left: 8px; opacity: .9; }
.k-fee .k-cost { margin-right: 6px; font-weight: 600; }
.k-costrow { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.k-reroll { display: flex; justify-content: flex-end; }
.k-upkeep {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  margin-top: 12px; padding-top: 12px; border-top: 1px dashed rgba(122, 90, 58, .35);
}
.k-upkeep-due { border-top-color: #c0523c; }
.k-upkeep-due .label-caps { color: #c0523c; }

/* (Aug 7 2026) The paired marble-temple headers are gone from the Koinon
   action cards — same render twice, a third of each card, no information. */
.koinon-request-row .grow { display: flex; flex-direction: column; gap: 6px; }
.koinon-request-row .gold-bar { width: 100%; }

/* ----- Marble nameplate buttons, batch 2 (stitch_agon_strategy_game_ui 6) -----
   These came in a different proportion to the action-rail plaques above: the
   five single buttons are roughly 1.4:1 and the four battle controls 3:1,
   whereas .btn-plaque-build/-editlayout/-progress are all ~2.5:1. Each group
   is therefore sized on its OWN aspect rather than forced into a shared box —
   stretching a 1.37:1 plaque into a 2.5:1 slot squashes the carved lettering
   badly enough to read as a rendering bug.
   The text is baked into the art, so the markup carries no label. */
.btn-plaque-finishnow   { background-image: url(/assets/ui/btn_finish_now.png?v=1);   width: 132px; height: 96px; }
.btn-plaque-claimweekly { background-image: url(/assets/ui/btn_claim_weekly.png?v=1); width: 150px; height: 90px; }
.btn-plaque-savegarrison{ background-image: url(/assets/ui/btn_save_garrison.png?v=1);width: 150px; height: 99px; }
.btn-plaque-tobattle    { background-image: url(/assets/ui/btn_to_battle.png?v=1);    width: 140px; height: 97px; }
/* battle controls — 3:1, sized to sit in the existing control row */
.btn-plaque-wait, .btn-plaque-defend, .btn-plaque-withdraw, .btn-plaque-autobattle {
  width: 124px; height: 41px;
}
.btn-plaque-wait       { background-image: url(/assets/ui/btn_wait.png?v=1); }
.btn-plaque-defend     { background-image: url(/assets/ui/btn_defend.png?v=1); }
.btn-plaque-withdraw   { background-image: url(/assets/ui/btn_withdraw.png?v=1); }
.btn-plaque-autobattle { background-image: url(/assets/ui/btn_autobattle.png?v=1); }
/* the art has no disabled state, so dim it the way the CSS buttons dim */
.btn-plaque:disabled { filter: grayscale(.6) brightness(.85); cursor: default; transform: none; }

/* Compact variant of the nameplate buttons. The Ambrosia "finish now" action
   appears twice — once in the building modal, where there is room for the full
   132x96 plaque, and once inline in an Army training row beside a progress bar,
   where that size would roughly triple the row height. Same art, same aspect,
   just scaled to sit in a dense row. */
.btn-plaque.compact { width: 74px; height: 54px; }

/* The second BUILD plaque (stitch_agon_strategy_game_ui 6). Its art is 1.37:1
   against the action rail's 2.5:1 trio, so it is NOT used there — it would be
   squashed and would no longer match Edit Layout and Progress beside it.
   It fits the building modal instead: that slot shows either UPGRADE or BUILD
   depending on whether the building exists, never both at once, so the
   different proportion never appears next to its sibling. */
.btn-plaque-buildtall { background-image: url(/assets/ui/btn_build_tall.png?v=1); width: 116px; height: 85px; }

/* ----- Building modal hero (round 30-follow-up-58) -----
   The portrait sits in the same carved medallion frame used for unit portraits,
   scaled up so a full building render reads as artwork rather than as a list
   thumbnail. The frame art is a square bronze surround, so the image is inset
   by its border width and clipped to a soft square. */
.bld-hero { display: flex; gap: 18px; align-items: center; padding: 4px 0 14px; }
.bld-portrait {
  position: relative; flex-shrink: 0; width: 156px; height: 156px;
  display: flex; align-items: center; justify-content: center;
  background: url(/assets/ui/medallion_frame.png) center/100% 100% no-repeat;
}
.bld-portrait-art {
  width: 74%; height: 74%; object-fit: contain;
  filter: drop-shadow(0 3px 5px rgba(0, 0, 0, .35));
}
.bld-hero-text { min-width: 0; }
.bld-title { font-size: 26px; line-height: 1.15; margin: 0 0 4px; }
.bld-level {
  display: inline-block; padding: 3px 10px; border-radius: 3px;
  background: rgba(85, 67, 54, .12); border: 1px solid var(--outline-variant);
  color: var(--on-surface-variant); margin-bottom: 7px;
}
.bld-level-new { background: rgba(141, 75, 0, .1); }
.bld-desc { font-size: 15px; line-height: 1.45; }
@media (max-width: 560px) {
  .bld-hero { gap: 12px; }
  .bld-portrait { width: 108px; height: 108px; }
  .bld-title { font-size: 21px; }
}

/* ----- territory switcher (round 30-follow-up-59) -----
   Only rendered when the account owns more than one polis, so single-territory
   players see no change at all. */
.polis-switch { display: flex; gap: 4px; margin-left: 14px; align-items: center; }
.polis-tab {
  font: 700 11px/1 'Source Sans 3', sans-serif; letter-spacing: .08em; text-transform: uppercase;
  padding: 6px 11px; border-radius: 3px; cursor: pointer;
  border: 1px solid var(--outline-variant); background: rgba(255, 255, 255, .55);
  color: var(--on-surface-variant); white-space: nowrap;
}
.polis-tab:hover { background: rgba(255, 255, 255, .8); }
.polis-tab.active {
  background: var(--primary); border-color: var(--primary); color: var(--on-primary);
  box-shadow: 0 1px 3px rgba(0, 0, 0, .3);
}

/* ─────────────────────────────────────────────────────────────────────────
   Confirm-a-spend dialog (Aug 4 2026 — "after I click on the mission it will
   display how much resources it will cost, and then I can agree or cancel
   it"). Rendered by confirmCost() in app.js and shared by every irreversible
   spend: launching a raid or campaign mission, and paying Ambrosia to finish
   a building or a training batch early. Sits inside the normal .modal box, so
   it inherits the marble panel; these rules only lay out the bill and the two
   actions.

   Cancel is the plain button and Confirm the primary one, in that order, so
   the destructive-by-default reading is "nothing happens unless you reach for
   the gold one".
   ───────────────────────────────────────────────────────────────────────── */
.confirm-cost { min-width: min(420px, 80vw); }
.confirm-cost-body { color: var(--on-surface-var); margin-bottom: 14px; }
.confirm-cost-bill {
  border: 1px solid var(--outline-var); border-radius: 10px;
  background: rgba(212, 160, 23, .10);
  padding: 12px 14px; margin-bottom: 18px;
  display: flex; align-items: center; justify-content: space-between; gap: 16px;
}
.confirm-cost-amounts .cost { font-size: 1.15em; }
.confirm-cost-actions { display: flex; gap: 10px; justify-content: flex-end; }
.confirm-cost-actions .btn { min-width: 118px; justify-content: center; }

/* Mission / target briefing block inside that dialog — what you are walking
   into and what you win, above what it costs. */
.mission-brief {
  display: grid; gap: 6px; margin-bottom: 12px;
  border-left: 3px solid var(--gold-bright); padding-left: 12px;
}
.mission-brief-line { display: flex; gap: 10px; align-items: baseline; }
.mission-brief-line .label-caps { min-width: 84px; flex: none; }
.mission-brief-reward { color: var(--primary); font-weight: 600; }
/* Aug 12 2026 — the same briefing, shown in the army picker rather than only in
   the confirm dialog, so the defenders are on screen while you choose. */
.picker-brief {
  margin-bottom: 14px; padding: 12px 14px;
  border: 1px solid var(--outline-var); border-radius: 8px; background: var(--surface-low);
}
.picker-brief .mission-brief { margin-bottom: 0; }

/* ─────────────────────────────────────────────────────────────────────────
   Raid page, game-friendly pass (Aug 4 2026 — "Make Raid UI more game
   friendly"). Targets were text rows reading "Glory N · visible loot ≈N" with
   two same-weight buttons; they are cards now — Acropolis portrait, threat
   pips, the three numbers that actually decide a target, and one obvious
   primary action. Battle reports get a win/loss colour edge so the outcome
   reads without parsing the sentence.
   ───────────────────────────────────────────────────────────────────────── */
.raid-page .raid-reports { display: flex; flex-direction: column; gap: 8px; }
.raid-report {
  display: flex; gap: 12px; align-items: center;
  padding: 10px 12px; border-radius: 10px;
  border: 1px solid rgba(219, 194, 176, .3); border-left-width: 4px;
  background: rgba(255, 253, 242, .12);
  color: #f2e9d8;
}
.raid-report b { color: #fff6e2; }
.raid-report .muted { color: #cfc2ab; }
.raid-report.is-win { border-left-color: #5c8a3a; }
.raid-report.is-loss { border-left-color: var(--error); }
.raid-report-icon { width: 34px; height: 34px; object-fit: contain; flex: none; }
.raid-report-main { flex: 1; min-width: 0; }
.raid-report-meta { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 4px; }
.raid-chip {
  font-size: .76rem; padding: 2px 8px; border-radius: 999px;
  background: var(--surface-high); border: 1px solid var(--outline-var);
  color: var(--on-surface-var);
}
.raid-chip-loot { background: rgba(212, 160, 23, .18); border-color: var(--gold); }
.raid-chip-up { background: rgba(92, 138, 58, .18); border-color: #5c8a3a; color: #3d6127; }
.raid-chip-down { background: rgba(186, 26, 26, .12); border-color: var(--error); color: var(--error); }
.raid-report-age { flex: none; font-size: .8rem; color: #cfc2ab; }
.raid-page .raid-reports > .muted { color: #d8ccb6; }
.raid-page .raid-chip { background: rgba(255,255,255,.14); border-color: rgba(219,194,176,.35); color: #ece2cf; }
.raid-page .raid-chip-loot { background: rgba(212,160,23,.3); border-color: var(--gold-bright); color: #ffeab0; }
.raid-page .raid-chip-up { background: rgba(92,138,58,.34); border-color: #8ec46a; color: #d8f0c4; }
.raid-page .raid-chip-down { background: rgba(186,26,26,.32); border-color: #e46a6a; color: #ffd4d4; }

@media (max-width: 700px) {
  .raid-page .raid-targets { grid-template-columns: 1fr; max-height: 300px; }
  .raid-target-stats { grid-template-columns: 1fr 1fr; }
}

/* Resource bundles + tiered legendary cards (Aug 4 2026 — "Add resource
   bundles to the shop", "Make them so they are in different price ranges"). */
.bundle-lines { display: flex; flex-wrap: wrap; gap: 6px; justify-content: center; }
.bundle-line {
  display: inline-flex; align-items: center; gap: 4px;
  font-size: .84rem; padding: 2px 8px; border-radius: 999px;
  background: var(--surface-high); border: 1px solid var(--outline-var);
}
.bundle-line img { width: 15px; height: 15px; object-fit: contain; }
.bundle-line-ico { width: 15px; height: 15px; object-fit: contain; }

/* Booster bundles show one render per effect they grant, the same way a
   creature bundle shows every unit type it contains (Aug 7 2026 — "still
   everything are simple icons"). Four effects still fit one card because the
   circles shrink as the row grows. */
/* Booster bundles show one render per effect they grant, sized down as the
   row grows so four circles still fit one card. */
.bundle-art {
  display: flex; justify-content: center; align-items: center;
  flex-wrap: wrap; gap: 6px; min-height: 96px;
}
/* …but NOT inside the flex row: auto inline margins on flex items eat the
   free space and blow the renders apart. The row centres itself. */
.bundle-art img.big { width: 62px; height: 62px; margin-inline: 0; }
.bundle-art img.big:only-child { width: 92px; height: 92px; }
.bundle-art img.big:first-child:nth-last-child(n+4),
.bundle-art img.big:first-child:nth-last-child(n+4) ~ img.big { width: 44px; height: 44px; }
img.bundle-line-ico { border-radius: 50%; }

/* Framed character portraits — used for TALKING to the player (tutorial,
   dialogues), never as product art. The baked Greek ring is its own border. */
img.portrait { border: none; background: transparent; padding: 0; }

/* ----- the tutorial (Aug 7 2026) ----- */
/* A guide bubble pinned above the bottom nav: the speaking character's framed
   portrait beside a parchment scroll. It never blocks the page behind it —
   the player can poke at whatever the guide is pointing out. */
.tut-box {
  position: fixed; left: 50%; bottom: 96px; transform: translateX(-50%);
  /* Below every modal (100) and drawer (350): a dialog the player opens mid-
     tutorial covers the guide instead of the guide covering the dialog. */
  z-index: 60; display: flex; align-items: flex-end; gap: 14px;
  width: min(660px, calc(100vw - 28px));
  animation: tut-in .28s ease-out;
}
.tut-box.tut-out { opacity: 0; transform: translateX(-50%) translateY(14px); transition: all .22s ease; }
@keyframes tut-in { from { opacity: 0; transform: translateX(-50%) translateY(18px); } }
.tut-face { width: 108px; height: 108px; flex: 0 0 auto; }
.tut-scroll {
  flex: 1; padding: 14px 16px 12px; border-radius: 12px;
  background: linear-gradient(180deg, #f8f2e2, #eadcbe);
  border: 2px solid #7a5a3a;
  box-shadow: inset 0 0 0 1.5px rgba(212,160,23,.4), 0 12px 32px rgba(20,12,4,.5);
  color: #2a2013;
}
.tut-head { display: flex; justify-content: space-between; align-items: baseline; margin-bottom: 4px; }
.tut-who { color: #6b4d12; font-weight: 800; }
.tut-count { font-size: 12px; }
.tut-text { font-size: 14.5px; line-height: 1.5; }
.tut-actions { display: flex; gap: 8px; justify-content: flex-end; margin-top: 10px; }
@media (max-width: 640px) {
  .tut-box { bottom: 84px; }
  .tut-face { width: 74px; height: 74px; }
}

/* Character introductions: same bubble, one OK. `tut-high` floats a
   building's greeting above that building's own scroll modal. */
.tut-box.tut-high { z-index: 390; }


/* The Custom market, inline under the filter buttons instead of a modal
   (Aug 7 2026 — "make so it is open below, not in the new small window"). */
#custom-host .custom-inline { max-width: 760px; margin: 0 auto 16px; animation: rise .18s ease-out; }

/* The booster's strength, written as a plain percentage on the card. */
.boost-pct {
  font-weight: 800; font-size: 1.06rem; letter-spacing: .03em;
  color: var(--gold, #8a6d1f); margin: 2px 0 2px;
}
.boost-pct-for { font-weight: 600; font-size: .82rem; opacity: .75; letter-spacing: .05em; }

/* The price tag doubles as the tier badge: richer metal as the price climbs,
   so the roster reads as a ladder at a glance instead of seven equal cards
   with different numbers on the buttons. */
.card.legend { position: relative; }
.legend-price-tag {
  position: absolute; top: 10px; right: 10px;
  font-size: .78rem; font-weight: 700; letter-spacing: .02em;
  padding: 3px 9px; border-radius: 999px;
  background: var(--surface-high); border: 1px solid var(--outline);
  color: var(--on-surface-var);
}
.card.legend-t5   .legend-price-tag { background: #e7e3d5; border-color: #b6ae95; }
.card.legend-t10  .legend-price-tag { background: #dfe6df; border-color: #8fa88f; }
.card.legend-t20  .legend-price-tag { background: #dbe4ee; border-color: #7f9bb8; }
.card.legend-t30  .legend-price-tag { background: #ece0d2; border-color: #b58b5c; color: #6b4a22; }
.card.legend-t50  .legend-price-tag { background: rgba(212,160,23,.28); border-color: var(--gold); color: #6b5108; }
.card.legend-t100 .legend-price-tag {
  background: linear-gradient(135deg, #f2d98a, #d4a017 55%, #f2d98a);
  border-color: #8e6b06; color: #4a3703;
  box-shadow: 0 0 10px rgba(212,160,23,.55);
}
.card.legend-t100 { box-shadow: 0 0 0 1px var(--gold-bright), inset 0 0 22px rgba(212,160,23,.22); }
.legend-stats { display: flex; gap: 12px; justify-content: center; flex-wrap: wrap; color: var(--on-surface-var); }
/* …but on the wooden card that body colour measured 1.53:1. */
.cards > .card .legend-stats { color: #e4d5b4; }
/* Same reason: the booster percentages used the dark-panel gold, which lands
   at 4.39:1 on wood — a hair under the AA line. */
.cards > .card .boost-pct, .cards > .card .boost-pct-for { color: #e8bd52; }
.legend-stats span { display: inline-flex; align-items: center; gap: 4px; }

/* A modal that has another modal open on top of it (see modal() in app.js).
   Hidden rather than closed so the one underneath keeps its state — the army
   picker holds your chosen stacks while the confirm dialog asks about them. */
.modal-back.modal-covered { display: none; }

/* ── Raid page, Aug 4 2026 rebuild ─────────────────────────────────────────
   "when in raid it will have button RAID and under history battle report."
   The page is one action plus your own history; picking a target happens in
   a modal after you press RAID, not in a grid you have to read first.
   ───────────────────────────────────────────────────────────────────────── */
/* The button IS the carved plaque (ui/btn_raid.png — "RAID" and the crossed
   swords are part of the art), so the element is a bare transparent frame with
   no border, background or label of its own. Anything drawn behind it would
   show as a rectangle around a shape that is not rectangular. */
.raid-big-btn {
  width: min(460px, 92%); padding: 0; cursor: pointer; display: block;
  border: 0; background: none; line-height: 0;
  filter: drop-shadow(0 5px 10px rgba(40, 28, 12, .45));
  transition: transform .1s, filter .1s;
}
.raid-big-btn-art { width: 100%; height: auto; display: block; }
.raid-big-btn:hover { filter: drop-shadow(0 6px 14px rgba(40,28,12,.55)) brightness(1.06); }
.raid-big-btn:active { transform: translateY(3px); filter: drop-shadow(0 2px 5px rgba(40,28,12,.5)); }
.raid-big-btn-sub { font-size: .86rem; color: var(--on-surface-var); letter-spacing: .04em; }

.raid-seeking { min-width: min(380px, 78vw); display: grid; gap: 12px; justify-items: center; padding: 8px 0; }
.raid-seeking-text { color: var(--on-surface-var); }

/* Target screen */
.raid-target-view { min-width: min(560px, 84vw); }
.raid-target-head {
  display: flex; align-items: flex-start; justify-content: space-between;
  gap: 16px; margin-bottom: 14px;
  /* clears the modal's own ✕, which is absolutely positioned top-right and
     was sitting on top of the Glory label */
  padding-right: 34px;
}
.raid-glory { text-align: right; display: grid; gap: 2px; }
.raid-glory b { font-size: 1.35rem; color: var(--primary); }

/* The village diorama. Sprites are absolutely placed against a fixed-height
   strip so a level-1 Acropolis and a level-8 one both sit ON the ground line
   instead of floating or overflowing — bottom-anchored, height-capped, width
   free. Same reason the town renderer bottom-anchors: the ground is the one
   edge that must agree. */
.raid-village {
  position: relative; height: 190px; margin-bottom: 16px; border-radius: 12px;
  border: 1px solid var(--outline-var); overflow: hidden;
  background: linear-gradient(#bcd2e8 0%, #cfdcc4 52%, #a9b184 100%);
}
.raid-village-ground {
  position: absolute; left: 0; right: 0; bottom: 0; height: 46%;
  background: radial-gradient(ellipse at 50% 0%, #b6ba7e, #9aa06c 70%, #8b9160);
}
.raid-village img { position: absolute; bottom: 14px; object-fit: contain; }
.rv-acropolis { left: 50%; transform: translateX(-50%); height: 140px; z-index: 2; }
.rv-tower { height: 96px; z-index: 1; opacity: .96; }
.rv-tower-l { left: 8%; }
.rv-tower-l ~ .rv-tower-l { left: 22%; height: 84px; }
.rv-tower-r { right: 8%; }
.rv-tower-r ~ .rv-tower-r { right: 22%; height: 84px; }
.rv-shield {
  position: absolute; top: 10px; right: 10px; z-index: 3;
  background: rgba(28, 99, 144, .9); color: #fff; border-radius: 999px;
  padding: 4px 12px; font-size: .82rem;
}

.raid-target-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-bottom: 10px; }
.raid-stat-box {
  display: grid; gap: 3px; padding: 10px 12px; border-radius: 10px;
  border: 1px solid var(--outline-var); background: var(--surface-low);
}
.raid-stat-box b { font-size: .95rem; }
.raid-def-box { position: relative; display: flex; align-items: center; gap: 10px; }
.raid-def-box > div { display: grid; gap: 3px; }
.raid-def-icon { width: 42px; height: 42px; object-fit: contain; flex: none; }
.raid-def-box b { font-size: 1.25rem; }
.raid-def-band { font-size: .8rem; font-weight: 600; }
.raid-def-box[data-band="Lightly held"] { background: rgba(92,138,58,.14); border-color: #5c8a3a; }
.raid-def-box[data-band="Evenly matched"] { background: rgba(212,160,23,.14); border-color: var(--gold); }
.raid-def-box[data-band="Evenly matched"] .raid-def-band { color: #6b5108; }
.raid-def-box[data-band="Lightly held"] .raid-def-band { color: #3d6127; }
.raid-def-box[data-band="Moderate"]     { background: rgba(212,160,23,.14); border-color: var(--gold); }
.raid-def-box[data-band="Moderate"] .raid-def-band { color: #6b5108; }
.raid-def-box[data-band="Strong"]       { background: rgba(210,118,42,.16); border-color: #d2762a; }
.raid-def-box[data-band="Strong"] .raid-def-band { color: #a75a1c; }
.raid-def-box[data-band="Formidable"],
.raid-def-box[data-band="Overwhelming"] { background: rgba(186,26,26,.12); border-color: var(--error); }
.raid-def-box[data-band="Formidable"] .raid-def-band,
.raid-def-box[data-band="Overwhelming"] .raid-def-band { color: var(--error); }

.raid-fog-note { font-size: .82rem; margin-bottom: 16px; }
.raid-target-view-actions { display: flex; gap: 10px; justify-content: flex-end; }
.raid-target-view-actions .btn { min-width: 150px; justify-content: center; }

@media (max-width: 700px) {
  .raid-target-grid { grid-template-columns: 1fr; }
  .raid-village { height: 150px; }
  .rv-acropolis { height: 108px; }
  .rv-tower { height: 74px; }
}


/* Inline icon inside a .btn (crossed swords on Attack, etc.) — sized to the
   text it sits beside rather than a fixed px, so it tracks button scale. */
.btn .btn-ico { width: 1.35em; height: 1.35em; object-fit: contain; vertical-align: -.3em; margin-right: 2px; }

/* Modal close: the carved marble X replaces the ✕ glyph. The button keeps its
   existing hit area; only what is drawn inside changes. */
/* The button had no intrinsic size — it was sized by the ✕ glyph's font-size.
   Swapping in an <img> removed the only thing giving it dimensions, so the
   marble X ballooned to its natural 190px and sat across the modal's header.
   An explicit box is now required, not optional. */
.modal .close {
  width: 34px; height: 34px; padding: 0;
  display: flex; align-items: center; justify-content: center;
  filter: drop-shadow(0 1px 2px rgba(40, 28, 12, .4));
  transition: transform .1s, filter .1s;
}
.modal .close img { width: 100%; height: 100%; object-fit: contain; display: block; }
.modal .close:hover { transform: scale(1.08); filter: drop-shadow(0 2px 4px rgba(40,28,12,.55)); }
.modal .close:active { transform: scale(.96); }

/* ── Raid search transition + tiered target (Aug 4 2026) ───────────────────
   "after I press raid there will be a loading screen with loading bar on the
   bottom." Distinct from battleLoading()'s centred box on purpose: this one is
   a search, that one is a march, and they should not look like the same event.
   ───────────────────────────────────────────────────────────────────────── */
.raid-loading {
  position: fixed; inset: 0; z-index: 400; overflow: hidden;
  background: #140f08;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 18px; padding: 0 6vw 9vh;
  opacity: 0; transition: opacity .26s ease;
}
/* A touch darker than the march overlay: this one carries a quote to read. */
.raid-loading .loading-scrim { background: linear-gradient(rgba(20, 15, 8, .55), rgba(20, 15, 8, .72)); }
.raid-loading.is-in { opacity: 1; }
.raid-loading.is-ready { cursor: pointer; }

/* The quote sits where the eye lands — middle of the screen — while the search
   state stays pinned to the bottom edge. */
.raid-quote {
  margin: 0; max-width: min(760px, 88vw); text-align: center;
  animation: raid-quote-in .7s ease both;
}
.raid-quote-text {
  margin: 0; color: #f7eeda; font-family: var(--serif);
  font-size: clamp(1.15rem, 2.5vw, 1.9rem); line-height: 1.45; font-style: italic;
  text-shadow: 0 3px 14px rgba(0, 0, 0, .85);
}
.raid-quote-text::before { content: '\201C'; }
.raid-quote-text::after { content: '\201D'; }
.raid-quote-author {
  margin-top: 14px; color: var(--gold-bright); letter-spacing: .16em;
  text-transform: uppercase; font-size: .78rem;
  text-shadow: 0 2px 8px rgba(0, 0, 0, .85);
}
@keyframes raid-quote-in {
  from { opacity: 0; transform: translateY(14px); }
  to   { opacity: 1; transform: none; }
}

.raid-loading-foot {
  position: absolute; left: 0; right: 0; bottom: 9vh;
  display: flex; flex-direction: column; align-items: center; gap: 14px;
  padding: 0 6vw;
}
.raid-loading-text {
  color: #f6ecd8; font-family: var(--serif); font-size: 1.5rem;
  text-shadow: 0 3px 10px rgba(0, 0, 0, .8); text-align: center;
}
.raid-loading-bar { width: min(680px, 90vw); }

/* Ready: the bar and its caption give way to the prompt. Both leave the flow
   rather than dimming, so the prompt lands where the bar was and nothing
   jumps. */
.raid-loading-press { display: none; }
.raid-loading.is-ready .raid-loading-text,
.raid-loading.is-ready .raid-loading-bar { display: none; }
.raid-loading.is-ready .raid-loading-press {
  display: block; color: #f7eeda; font-family: var(--serif);
  font-size: 1.15rem; letter-spacing: .1em; text-transform: uppercase;
  text-shadow: 0 3px 12px rgba(0, 0, 0, .85);
  animation: raid-press-pulse 1.6s ease-in-out infinite;
}
@keyframes raid-press-pulse {
  0%, 100% { opacity: .55; }
  50%      { opacity: 1; }
}
@media (prefers-reduced-motion: reduce) {
  .raid-quote { animation: none; }
  .raid-loading.is-ready .raid-loading-press { animation: none; opacity: 1; }
}

/* Defending line-up, in front of the village. Counts are "?" — the server
   never sends the real numbers, so there is nothing here to reveal. */
.rv-lineup {
  position: absolute; left: 0; right: 0; bottom: 6px; z-index: 3;
  display: flex; justify-content: center; align-items: flex-end; gap: 8px; flex-wrap: wrap;
  padding: 0 10px;
}
.rv-unit { position: relative; width: 46px; height: 46px; }
.rv-unit img {
  position: static; width: 100%; height: 100%; object-fit: contain;
  filter: drop-shadow(0 2px 4px rgba(0, 0, 0, .5));
}
.rv-unit-count {
  position: absolute; right: -4px; bottom: -4px;
  min-width: 20px; height: 20px; padding: 0 4px; border-radius: 999px;
  background: rgba(20, 15, 8, .88); color: var(--gold-bright);
  border: 1px solid var(--gold); font-size: .8rem; font-weight: 700;
  display: flex; align-items: center; justify-content: center;
}
.rv-nogarrison { color: #f0e6d2; text-shadow: 0 2px 5px rgba(0,0,0,.7); font-size: .9rem; }

/* Power tier band — the one line that says how bad an idea this is. */
.raid-tier {
  display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap;
  padding: 9px 14px; border-radius: 10px; margin-bottom: 12px;
  border: 1px solid var(--outline-var); background: var(--surface-low);
}
.raid-tier-label { font-family: var(--serif); font-size: 1.15rem; font-weight: 700; }
.raid-tier-note { font-size: .85rem; color: var(--on-surface-var); }
.raid-tier[data-mult="0.5"], .raid-tier[data-mult="0.7"] { background: rgba(92,138,58,.16); border-color: #5c8a3a; }
.raid-tier[data-mult="0.5"] .raid-tier-label, .raid-tier[data-mult="0.7"] .raid-tier-label { color: #3d6127; }
.raid-tier[data-mult="0.9"], .raid-tier[data-mult="1"] { background: rgba(212,160,23,.16); border-color: var(--gold); }
.raid-tier[data-mult="0.9"] .raid-tier-label, .raid-tier[data-mult="1"] .raid-tier-label { color: #6b5108; }
.raid-tier[data-mult="1.2"] { background: rgba(210,118,42,.18); border-color: #d2762a; }
.raid-tier[data-mult="1.2"] .raid-tier-label { color: #a75a1c; }
.raid-tier[data-mult="1.5"], .raid-tier[data-mult="2"] { background: rgba(186,26,26,.14); border-color: var(--error); }
.raid-tier[data-mult="1.5"] .raid-tier-label, .raid-tier[data-mult="2"] .raid-tier-label { color: var(--error); }

.raid-cost-label { opacity: .85; }
.raid-page .raid-big-btn-sub .cost { vertical-align: -2px; }
.raid-page .raid-big-btn-sub .cost img { width: 17px; height: 17px; }

/* ── Admin cabinet + player account (Aug 5 2026) ───────────────────────────
   Deliberately plainer than the rest of the game: this is a back office, and
   marble plaques over a data table make numbers harder to read, not easier.
   It still lives inside .panel so it belongs to the same world.
   ───────────────────────────────────────────────────────────────────────── */
/* Pushed to the right-hand end: .rescell-bar is absolutely centred on the whole
   bar, so anything that stays in the flow has to keep out of the middle. The
   z-index keeps these clickable where the plaque's box overlaps them. */
.topbar-links { display: flex; gap: 6px; margin-left: auto; position: relative; z-index: 2; }
.topbar-link {
  font-family: var(--serif); font-size: .82rem; letter-spacing: .1em; text-transform: uppercase;
  color: var(--on-surface-var); text-decoration: none;
  padding: 5px 11px; border: 1px solid var(--outline-var); border-radius: 6px;
  background: rgba(255, 253, 244, .72); white-space: nowrap;
}
.topbar-link:hover { border-color: var(--gold); color: var(--primary); }
.topbar-link.active { border-color: var(--gold); background: var(--primary-fixed); color: var(--primary); }
.topbar-float .topbar-link { background: rgba(28, 20, 10, .55); color: #f2e8d4; border-color: rgba(212, 160, 23, .5); }

.admin-page { display: flex; flex-direction: column; gap: 16px; padding-bottom: 96px; }
/* The bottom nav is fixed over the page, so both cabinets keep a lane clear
   under their last row — otherwise the final table row sits behind the
   plaques and reads as cut off. */
.admin-tabs {
  display: flex; flex-wrap: wrap; gap: 4px;
  padding: 6px; border-radius: 10px;
  background: var(--surface-cont); border: 1px solid var(--outline-var);
}
.admin-tab {
  padding: 7px 14px; border-radius: 7px; text-decoration: none;
  font-family: var(--serif); font-size: .92rem; color: var(--on-surface-var);
}
.admin-tab:hover { background: var(--surface-high); }
.admin-tab.active { background: var(--primary); color: #fff; }
.admin-loading { padding: 40px; text-align: center; }

.admin-panel { margin-bottom: 16px; }
.admin-panel-head {
  display: flex; align-items: flex-start; justify-content: space-between; gap: 16px;
  margin-bottom: 14px; flex-wrap: wrap;
}
.admin-panel-head h2 { margin: 0; }
.admin-panel-actions { display: flex; gap: 8px; flex-wrap: wrap; }

.admin-stats { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 10px; }
.admin-stat {
  padding: 14px; border-radius: 10px; text-align: center;
  background: var(--surface-low); border: 1px solid var(--outline-var);
}
.admin-stat.accent { background: var(--primary-fixed); border-color: var(--gold); }
.admin-stat-value { font-family: var(--serif); font-size: 1.55rem; font-weight: 700; }
.admin-stat-label { font-size: .74rem; letter-spacing: .1em; text-transform: uppercase; color: var(--on-surface-var); }

.admin-table-bar { display: flex; align-items: center; gap: 12px; margin-bottom: 8px; }
.admin-search {
  flex: 1; max-width: 320px; padding: 7px 11px; border-radius: 7px;
  border: 1px solid var(--outline-var); background: var(--surface); font: inherit;
}
.admin-table-scroll { overflow-x: auto; }
.admin-table { width: 100%; border-collapse: collapse; font-size: .88rem; }
.admin-table th {
  text-align: left; padding: 8px 10px; white-space: nowrap; cursor: pointer;
  border-bottom: 2px solid var(--outline-var);
  font-size: .72rem; letter-spacing: .09em; text-transform: uppercase; color: var(--on-surface-var);
}
.admin-table th:hover { color: var(--primary); }
.admin-table th.sorted::after { content: ' ▲'; font-size: .7em; }
.admin-table th.sorted[data-dir=desc]::after { content: ' ▼'; }
.admin-table td { padding: 7px 10px; border-bottom: 1px solid var(--surface-high); vertical-align: middle; }
.admin-table tbody tr:hover { background: var(--surface-low); }
.admin-table td.num, .admin-table th.num { text-align: right; font-variant-numeric: tabular-nums; }
.admin-table td.actions { white-space: nowrap; text-align: right; }
.admin-empty { text-align: center; padding: 26px; }
.admin-table tr.is-blocked { background: rgba(186, 26, 26, .07); }
.admin-table tr.is-bot td { color: var(--on-surface-var); }
.admin-table tr.is-off td { opacity: .55; }
.admin-table tr.row-bad { background: rgba(186, 26, 26, .09); }
.admin-table tr.row-new { background: rgba(28, 99, 144, .08); }
.admin-row-actions { display: inline-flex; gap: 4px; }
.admin-thumb { width: 34px; height: 34px; object-fit: contain; }
.admin-code {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: .78rem;
  color: var(--on-surface-var); word-break: break-all;
}
.btn.btn-sm { padding: 3px 9px; font-size: 12px; }
.cell-edit {
  width: 88px; padding: 4px 7px; text-align: right; font: inherit;
  border: 1px solid var(--outline-var); border-radius: 5px; background: var(--surface);
}
.cell-edit.saving { opacity: .5; }
.cell-edit.saved { border-color: #2f7d43; box-shadow: 0 0 0 2px rgba(47, 125, 67, .2); }

.admin-badge {
  display: inline-block; padding: 1px 6px; border-radius: 4px; margin-left: 4px;
  font-size: .64rem; letter-spacing: .08em; font-weight: 700;
  background: var(--surface-high); color: var(--on-surface-var); vertical-align: 1px;
}
.admin-badge.bot { background: #dfe6ea; color: #3c5566; }
.admin-badge.admin { background: var(--primary); color: #fff; }
.admin-badge.founder { background: var(--gold); color: #2a1c05; }
.admin-badge.blocked { background: var(--error); color: #fff; }
.admin-pill {
  display: inline-block; padding: 2px 9px; border-radius: 999px;
  font-size: .72rem; letter-spacing: .06em; text-transform: uppercase;
  background: var(--surface-high); color: var(--on-surface-var);
}
.admin-pill.paid, .admin-pill.success, .admin-pill.sent { background: #d8ecdd; color: #1e5c31; }
.admin-pill.failed, .admin-pill.cancelled { background: #f6dcd9; color: #8f2318; }
.admin-pill.pending, .admin-pill.prep { background: #f7e9c8; color: #7a5a12; }
.admin-pill.battle { background: #f2d6c6; color: #8a3d18; }

.admin-bar { height: 8px; border-radius: 4px; background: var(--surface-high); min-width: 90px; }
.admin-bar-fill { height: 100%; border-radius: 4px; background: linear-gradient(90deg, var(--gold), var(--gold-bright)); }

.admin-legends { display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); gap: 12px; }
.admin-legend {
  padding: 12px; border-radius: 10px; text-align: center;
  background: var(--surface-low); border: 1px solid var(--outline-var);
}
.admin-legend.is-off { opacity: .55; }
.admin-legend img { width: 84px; height: 84px; object-fit: contain; }
.admin-legend-name { font-family: var(--serif); font-weight: 700; }
.admin-legend-stats { display: flex; justify-content: center; gap: 8px; font-size: .74rem; color: var(--on-surface-var); }
.admin-legend-price { font-weight: 700; margin-top: 4px; }
.admin-legend-owned { font-size: .74rem; }
.admin-legend-actions { display: flex; gap: 6px; justify-content: center; margin-top: 8px; }

/* Forms — shared by the admin modals and the account cabinet. */
.admin-form { min-width: min(420px, 78vw); }
.admin-form-wide { min-width: min(620px, 84vw); }
.admin-form-fields { display: flex; flex-direction: column; gap: 12px; margin-bottom: 12px; max-height: 62vh; overflow-y: auto; }
.admin-form-heading { margin: 8px 0 0; font-size: .9rem; letter-spacing: .08em; text-transform: uppercase; color: var(--on-surface-var); }
.admin-field { display: flex; flex-direction: column; gap: 4px; }
.admin-field-label { font-size: .74rem; letter-spacing: .09em; text-transform: uppercase; color: var(--on-surface-var); }
.admin-field input, .admin-field select, .admin-field textarea,
.admin-settings-form input, .admin-settings-form select, .admin-settings-form textarea {
  padding: 8px 11px; border-radius: 7px; border: 1px solid var(--outline-var);
  background: var(--surface); font: inherit; width: 100%;
}
.admin-field textarea { resize: vertical; }
.admin-field-check { flex-direction: row; align-items: center; gap: 8px; }
.admin-field-check input { width: auto; }
.admin-field-hint { font-size: .76rem; }
.admin-form-err { color: var(--error); min-height: 18px; font-size: .86rem; }

.admin-image-picker { display: flex; gap: 10px; align-items: center; }
.admin-image-preview {
  width: 62px; height: 62px; flex: 0 0 62px; border-radius: 8px;
  background: var(--surface-high); display: flex; align-items: center; justify-content: center;
}
.admin-image-preview img { max-width: 100%; max-height: 100%; object-fit: contain; }
.admin-image-controls { flex: 1; display: flex; gap: 8px; align-items: center; }
.admin-upload { cursor: pointer; white-space: nowrap; }

.admin-settings-form { display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: 12px; }
.admin-settings-actions { display: flex; gap: 8px; align-items: center; margin-top: 14px; flex-wrap: wrap; }
.admin-inline-input { padding: 7px 11px; border-radius: 7px; border: 1px solid var(--outline-var); background: var(--surface); font: inherit; }

/* Aug 12 2026 — the invite-only switch and the phone allowlist it consults.
   A checkbox row takes the full width of the settings grid: its label is a
   sentence, not a field name, and squeezed into a 260px column it wrapped
   into four lines beside a 13px box. */
.admin-settings-form > .admin-field-check { grid-column: 1 / -1; flex-wrap: wrap; }
.admin-field-check .admin-field-label {
  text-transform: none; letter-spacing: 0; font-size: .95rem;
  color: var(--on-surface); cursor: pointer;
}
.admin-field-check .admin-field-hint { flex-basis: 100%; margin-left: 22px; }
.admin-gate-warn {
  grid-column: 1 / -1; display: flex; gap: 12px; align-items: center; flex-wrap: wrap;
  padding: 11px 14px; border-radius: 9px; line-height: 1.5;
  border: 1px solid color-mix(in srgb, var(--error) 45%, transparent);
  background: color-mix(in srgb, var(--error) 8%, var(--surface));
}
.admin-gate-warn[hidden] { display: none; }

/* Contact details shared by more than one account — history the one-account
   rule could not rewrite. Folded away by default: it is a thing to look at
   when something is odd, not a permanent alarm. */
.admin-dupes { margin-top: 14px; }
.admin-dupes summary { cursor: pointer; font-size: .9rem; color: var(--on-surface-var); }
.admin-dupe-note { font-size: .84rem; margin: 10px 0; }
.admin-dupe-list { display: flex; flex-direction: column; }
.admin-dupe-row {
  display: grid; grid-template-columns: minmax(0, 240px) 70px minmax(0, 1fr);
  gap: 10px; align-items: baseline; padding: 6px 2px;
  border-bottom: 1px solid var(--outline-var); font-size: .87rem;
}
.admin-dupe-row:last-child { border-bottom: none; }
.admin-dupe-row b { overflow-wrap: anywhere; }

.admin-phone-add { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; margin-bottom: 12px; }
.admin-phone-add .admin-inline-input:first-child { flex: 0 0 200px; }
.admin-phone-add .admin-inline-input:nth-child(2) { flex: 1 1 220px; }
.admin-phone-search { margin-bottom: 8px; }
.admin-phone-search .admin-inline-input { width: 100%; max-width: 320px; }
.admin-phone-summary { font-size: .86rem; margin-bottom: 8px; }
.admin-phone-list { display: flex; flex-direction: column; }
.admin-phone-row {
  display: grid; grid-template-columns: 170px minmax(0, 1fr) minmax(0, 1fr) auto;
  gap: 10px; align-items: center;
  padding: 8px 2px; border-bottom: 1px solid var(--outline-var);
}
.admin-phone-row:last-child { border-bottom: none; }
/* The number is the identity of the row, so it is monospaced and does not wrap:
   two numbers differing in one digit have to line up to be comparable. */
.admin-phone-num { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: .92rem; white-space: nowrap; }
.admin-phone-note, .admin-phone-who { font-size: .86rem; overflow-wrap: anywhere; }
.admin-phone-bulk { margin-top: 14px; }
.admin-phone-bulk summary { cursor: pointer; font-size: .9rem; color: var(--on-surface-var); }
.admin-phone-bulk-body { display: flex; flex-direction: column; gap: 10px; align-items: flex-start; margin-top: 10px; }
.admin-phone-bulk-body textarea {
  width: 100%; padding: 10px; border-radius: 8px; border: 1px solid var(--outline-var);
  background: var(--surface); font: inherit; resize: vertical;
}
@media (max-width: 720px) {
  .admin-phone-row { grid-template-columns: 1fr auto; }
  .admin-phone-who { grid-column: 1 / -1; }
}

.admin-import { display: flex; flex-direction: column; gap: 12px; }
.admin-import-controls { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; }
.admin-import-text {
  width: 100%; padding: 10px; border-radius: 8px; border: 1px solid var(--outline-var);
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: .82rem; background: var(--surface);
}
.admin-import-problems {
  padding: 10px 12px; border-radius: 8px; margin-bottom: 10px;
  background: rgba(186, 26, 26, .08); color: #8f2318; font-size: .84rem;
}
.admin-letter { min-width: min(640px, 86vw); }
.admin-letter-frame { width: 100%; height: 52vh; border: 1px solid var(--outline-var); border-radius: 8px; background: #fff; }

/* Player cabinet */
.account-page { display: flex; flex-direction: column; gap: 16px; padding-bottom: 96px; }
/* Aug 13 2026 — "In account make all the categories on scroll background
   instead of white."
   Each category becomes its own parchment scroll, the same 9-slice every other
   in-game panel wears, at the compact .lfaq scale — the modal's 128/118
   shoulders would swallow a short card like Territories whole.

   Written as `.panel.account-panel`, two classes, NOT `.account-panel` alone.
   `.panel` carries the cream background and is declared later in this file, so
   a single-class selector here ties on specificity and loses on source order —
   the exact trap that has already cost this project the landing footer and the
   public pages' muted text. */
.panel.account-panel {
  border: solid transparent;
  border-width: 44px 38px 46px 38px;
  border-image: url('/assets/ui/scroll_frame.png?v=49') 175 160 175 160 fill / 44px 38px 46px 38px stretch;
  background: none;
  border-radius: 0;
  box-shadow: none;
  /* drop-shadow, not box-shadow: the scroll is a torn silhouette with rollers
     that overhang its box, and box-shadow would trace the rectangle instead of
     the parchment. */
  filter: drop-shadow(0 14px 34px rgba(16, 11, 4, .5));
  /* The border-image's slices stop at the SOLID parchment, but the art draws a
     meander band just inside that on all four sides. Without this padding the
     first row sits on the pattern and the action buttons run off the torn
     edge — which is exactly what the first attempt did. */
  padding: 10px 22px 14px;
}
/* The rules between rows were drawn for a cream card. On parchment they read
   as scratches at that weight, so they warm up and lighten. */
.panel.account-panel .cab-row { border-bottom-color: rgba(120, 92, 48, .28); }
/* Same for the little fact tiles: --surface-low is a cool near-white that
   fights the parchment underneath it. */
.panel.account-panel .account-fact {
  background: rgba(255, 250, 236, .5);
  border-color: rgba(120, 92, 48, .3);
}
@media (max-width: 720px) {
  .panel.account-panel {
    border-width: 30px 24px 32px 24px;
    border-image-width: 30px 24px 32px 24px;
    padding: 8px 12px 10px;
  }
}

.account-panel { padding: 20px; }
.account-head { display: flex; justify-content: space-between; align-items: flex-start; gap: 16px; flex-wrap: wrap; }
.account-head h2 { margin: 0; }
.account-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(170px, 1fr)); gap: 10px; margin: 16px 0; }
.account-fact {
  padding: 11px 13px; border-radius: 9px;
  background: var(--surface-low); border: 1px solid var(--outline-var);
  display: flex; flex-direction: column; gap: 3px;
}
.account-ok { color: #1e5c31; font-size: .76rem; }
.account-warn { color: #8f2318; font-size: .76rem; }
.account-actions { display: flex; gap: 8px; flex-wrap: wrap; }
.account-poleis { display: flex; flex-direction: column; gap: 8px; }
.account-polis {
  display: flex; justify-content: space-between; align-items: center; gap: 12px;
  padding: 11px 13px; border-radius: 9px;
  background: var(--surface-low); border: 1px solid var(--outline-var);
}
.account-polis.is-active { border-color: var(--gold); background: var(--primary-fixed); }
.account-history { display: flex; flex-direction: column; gap: 6px; }
.account-row {
  display: flex; justify-content: space-between; align-items: center; gap: 12px;
  padding: 9px 12px; border-radius: 8px; background: var(--surface-low);
}
.account-row-right { display: flex; align-items: center; gap: 10px; }

@media (max-width: 700px) {
  .admin-table { font-size: .8rem; }
  .admin-tab { padding: 6px 10px; font-size: .84rem; }
  .admin-settings-form { grid-template-columns: 1fr; }
}

/* ── Landing page (Aug 5 2026) ──────────────────────────────────────────────
   The front door for anyone not signed in: a hero over the raid footage with
   the two auth forms beside it, alternating feature bands below, and a footer
   carrying the legal links. Rendered bare — no top bar, no bottom nav.
   ───────────────────────────────────────────────────────────────────────── */
.landing { min-height: 100vh; background: #16110a; color: #f4ecdc; }
.landing-plain { display: flex; align-items: flex-start; justify-content: center; padding: 6vh 20px 60px; }

.landing-hero { position: relative; min-height: 100vh; display: flex; align-items: center; overflow: hidden; }
/* Aug 11 2026 — "Use all videos that you have, make so they all play for 6
   seconds and then blend to the next video." Two stacked <video> elements
   cross-fade inside .landing-reel; the drift keeps a six-second still from
   reading as a frozen frame. */
/* Aug 11 2026 — "make so videos are always there on the background and when you
   scroll down - they are still there, only the text scroll untill the footer."
   The reel is FIXED to the viewport rather than parked inside the hero, so the
   footage is the backdrop of the whole page and the words travel across it. */
.landing-reel { position: fixed; inset: 0; z-index: 0; overflow: hidden; pointer-events: none; }
.landing-reeled .landing-hero-scrim { position: fixed; inset: 0; z-index: 1; }
/* everything else rides above both */
.landing-reeled > *:not(.landing-reel):not(.landing-hero-scrim) { position: relative; z-index: 2; }
.landing-video {
  position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover;
  z-index: 0; pointer-events: none;
  /* the fade lasts as long as HERO_BLEND_MS in landing.js — the two have to
     agree or the swap either cuts or lingers */
  opacity: 0; transition: opacity 2s ease-in-out;
  transform-origin: 50% 55%;
  animation: landing-drift 26s ease-in-out infinite alternate;
  /* Sep 3 2026 — "less dark and more bright". The clips are graded dark; a
     modest lift plus a little contrast and saturation makes them read as
     daylight rather than dusk, without blowing the highlights. */
  filter: brightness(1.28) contrast(1.04) saturate(1.08);
}
.landing-video.is-on { opacity: 1; }
/* which clip is up — four small marks, bottom right, no controls to fiddle */
.landing-reel-dots {
  position: absolute; right: 26px; bottom: 26px; z-index: 3;
  display: flex; gap: 7px;
}
.landing-reel-dot {
  width: 7px; height: 7px; border-radius: 50%;
  background: rgba(255, 246, 214, .3);
  box-shadow: 0 1px 3px rgba(0, 0, 0, .6);
  transition: background .5s, width .5s;
}
.landing-reel-dot.is-on { background: var(--gold, #d4a017); width: 20px; border-radius: 4px; }
/* The cast band's styles lived here until Aug 11 2026 — "Remove all models of
   the creatures". The @keyframes and .landing-sprite went with it; nothing
   else on the site drives a sprite sheet from CSS. */

/* Aug 12 2026 — "Instead paper background use scroll and wooden frames for each
   seperate segment."

   The papyrus sheet that used to be behind the pitch is gone; the pitch hangs
   on the same parchment SCROLL the Koinon and the war rules use
   (scroll_frame.png, the .k-scroll 9-slice at its small scale — 88/74 rather
   than 128/118, because a hero column is 700px and full-size shoulders would
   take a third of it). Ink stays parchment-dark, as it was on the paper.

   The crest above it is deliberately UNframed: it is the one thing on the page
   that should be printed straight onto the footage. */
.landing-reeled .landing-pitch {
  border: solid transparent;
  border-width: 88px 74px 90px 74px;
  border-image: url('/assets/ui/scroll_frame.png?v=49') 175 160 175 160 fill / 88px 74px 90px 74px stretch;
  background: none;
  padding: 0;
  filter: drop-shadow(0 18px 44px rgba(16, 11, 4, .55));
}
/* The torn parchment edge wanders further in than the border is drawn, so the
   frame alone never guarantees a margin — the same lesson the Koinon scrolls
   taught. */
.landing-pitch-inner { padding: 0 26px; }
.landing-reeled .landing-lede { color: #4a3a24; }
.landing-reeled .landing-points li { color: #4a3a24; }
.landing-reeled .landing-hero-note { color: #6d5a3e; }
@media (max-width: 720px) {
  .landing-reeled .landing-pitch {
    border-width: 62px 40px 64px 40px;
    border-image-width: 62px 40px 64px 40px;
  }
  .landing-pitch-inner { padding: 0 8px; }
}

/* ── the masthead ─────────────────────────────────────────────────────────
   Aug 12 2026 — "For landing - make big Agon logo and AGON RISE OF THE
   POLEIS." It used to be a 132px emblem and a line of type sharing a column
   with the sign-in card, so the name of the game was smaller than the word
   "Password" beside it. Full width, and as large as the viewport allows. */
.landing-crest { text-align: center; margin-bottom: clamp(20px, 3.4vh, 40px); }
/* Sized against the viewport's HEIGHT as well as its width. A crest that only
   watches vw grows on a wide laptop screen until it has pushed the sign-in
   card off the bottom of a 950px window — which is the one thing on this page
   that must stay above the fold. */
/* Aug 12 2026 — the emblem was reported, repeatedly, as having a square behind
   it. The file is provably a circle with transparent corners, it renders clean
   here at four viewport sizes and at 2x, and nothing on the page paints a box
   over it — so whatever is drawing that square, it is not something I can see
   from this side.
   So stop arguing with it and make it impossible. The subject IS a circle
   inscribed in a square canvas, so clipping the element to a circle costs
   nothing and means no browser can paint anything in those corners however it
   decodes the PNG. The shadow moves from `filter: drop-shadow` — which follows
   the alpha channel, and is therefore exactly what would betray a bad one — to
   a `box-shadow`, which follows the border-radius and cannot be square. It is
   composited rather than re-rasterised, which this project already prefers.
   The tradeoff: a custom emblem uploaded in the admin is clipped round too.
   That is the right shape for the slot it sits in, but it is a change. */
/* Sep 3 2026 — "I want to have very big shiny logo." With the pitch column and
   the sign-in card gone from the hero, the emblem has the whole screen: sized
   against BOTH axes so it fills a wide monitor without pushing the title off a
   short one. The wrapper owns the circular clip and the sheen; the image inside
   it is untouched artwork. */
.landing-logo-wrap {
  position: relative;
  width: clamp(240px, min(38vw, 44vh), 560px);
  aspect-ratio: 1;
  margin: 0 auto;
  border-radius: 50%;
  overflow: hidden;
  cursor: pointer;
  isolation: isolate;
  /* Two shadows: a warm gold halo that breathes, and a plain drop shadow to
     seat the coin against the footage behind it. */
  box-shadow: 0 0 0 rgba(212, 160, 23, 0), 0 18px 60px rgba(0, 0, 0, .7);
  animation: landing-logo-glow 4.2s ease-in-out infinite;
}
.landing-crest .landing-logo {
  width: 100%; height: 100%;
  object-fit: contain;
  display: block;
  border-radius: 50%;
  filter: none;
  box-shadow: none;
}
/* The sheen: a narrow bright band raked across the coin and off the far side,
   then a long pause. Inside the wrapper's clip, so it never draws a rectangle
   over the page. pointer-events:none keeps the click on the wrapper. */
.landing-logo-wrap::after {
  content: '';
  position: absolute; inset: -30%;
  pointer-events: none;
  background: linear-gradient(105deg,
    rgba(255, 255, 255, 0)   38%,
    rgba(255, 249, 224, .30) 46%,
    rgba(255, 255, 255, .72) 50%,
    rgba(255, 249, 224, .30) 54%,
    rgba(255, 255, 255, 0)   62%);
  transform: translateX(-120%);
  animation: landing-logo-sheen 5.6s ease-in-out infinite;
}
@keyframes landing-logo-sheen {
  0%      { transform: translateX(-120%); }
  38%,
  100%    { transform: translateX(120%); }
}
@keyframes landing-logo-glow {
  0%, 100% { box-shadow: 0 0 34px rgba(212, 160, 23, .28), 0 18px 60px rgba(0, 0, 0, .7); }
  50%      { box-shadow: 0 0 78px rgba(240, 198, 74, .55), 0 18px 60px rgba(0, 0, 0, .7); }
}
@media (prefers-reduced-motion: reduce) {
  .landing-logo-wrap { animation: none; box-shadow: 0 0 44px rgba(212, 160, 23, .34), 0 18px 60px rgba(0, 0, 0, .7); }
  .landing-logo-wrap::after { animation: none; opacity: 0; }
}
.landing-crest .landing-title {
  font-size: clamp(3rem, min(10vw, 11.5vh), 8rem);
  line-height: .96; margin: clamp(4px, 1.2vh, 16px) 0 0;
}
.landing-crest .landing-sub {
  font-size: clamp(1rem, 2.6vw, 2rem);
  letter-spacing: .34em; margin-top: clamp(4px, 1vh, 14px);
  /* the tracking pushes the last letter off-centre by one gap's width */
  text-indent: .34em;
}
/* The crest sits on the footage now rather than on a dark wash, so its type
   carries its own contrast. */
.landing-crest .landing-title {
  color: #fff6de;
  text-shadow: 0 2px 4px rgba(10, 7, 3, .75), 0 6px 30px rgba(10, 7, 3, .85);
}
.landing-crest .landing-sub {
  color: #f4e6c4;
  text-shadow: 0 2px 5px rgba(10, 7, 3, .8), 0 4px 18px rgba(10, 7, 3, .7);
}

/* ── The middle screen: one line of someone else's words ───────────────────
   Sep 3 2026 — "After logo make a quote of sonmeone and them join in/
   registration." It sits over the footage with no plate of its own; the type
   carries its own contrast the way the crest above it does. */
.landing-quote {
  min-height: 62vh;
  display: flex; align-items: center; justify-content: center;
  padding: clamp(30px, 7vh, 80px) 0;
}
.landing-quote-inner { width: min(860px, 88vw); margin: 0; text-align: center; }
.landing-quote blockquote {
  margin: 0;
  font-family: var(--serif);
  font-size: clamp(1.35rem, 3.4vw, 2.7rem);
  line-height: 1.3;
  color: #fff6de;
  text-shadow: 0 2px 5px rgba(10, 7, 3, .8), 0 6px 30px rgba(10, 7, 3, .85);
}
/* Quote marks as ornament rather than punctuation the copy has to carry, so an
   operator can type a plain line into the admin and still get a quotation. */
.landing-quote blockquote::before { content: '\201C'; }
.landing-quote blockquote::after  { content: '\201D'; }
.landing-quote figcaption {
  margin-top: clamp(12px, 2.4vh, 26px);
  font-size: clamp(.85rem, 1.5vw, 1.05rem);
  letter-spacing: .28em; text-indent: .28em;
  text-transform: uppercase;
  color: #f0dcae;
  text-shadow: 0 2px 5px rgba(10, 7, 3, .85);
}
/* An em dash before the name, drawn rather than typed, so the field holds only
   the name. */
.landing-quote figcaption::before { content: '\2014\00a0'; opacity: .7; }
@media (max-height: 720px) {
  .landing-quote { min-height: auto; padding: 50px 0; }
}

/* ── The last screen: sign in / create ─────────────────────────────────────
   Sep 3 2026 — "if you scroll down registration or sign in." A screen of its
   own so the scroll arrives somewhere deliberate, with the card centred in it
   rather than hanging off the top. min-height rather than height: the register
   form is taller than the sign-in one and must not be clipped. */
.landing-gate {
  min-height: 100vh;
  display: flex; align-items: center; justify-content: center;
  padding: clamp(40px, 8vh, 90px) 0;
}
.landing-gate-inner { width: min(470px, 92vw); }
.landing-gate .landing-auth { width: 100%; }
/* The card was right-aligned when it shared the hero with a pitch column. It
   owns the screen now, so it centres. */
.landing-gate .landing-card { max-width: none; margin-left: 0; margin-right: 0; }
@media (max-height: 720px) {
  .landing-gate { min-height: auto; padding: 60px 0; }
}

.landing-hero-row {
  display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 430px);
  gap: 34px; align-items: center;
}
@media (max-width: 900px) {
  .landing-hero-row { grid-template-columns: minmax(0, 1fr); gap: 10px; }
}

/* the door, in the hero, for someone who arrived already convinced */
.landing-hero-cta { margin-top: 22px; display: flex; align-items: center; gap: 16px; flex-wrap: wrap; }
.landing-hero-btn { font-size: 1.05rem; padding: 12px 26px; }
.landing-hero-note { font-size: 13px; color: rgba(255, 246, 214, .72); }
/* and the nudge that says the page continues */
.landing-scroll-cue {
  position: absolute; left: 50%; bottom: 22px; z-index: 3;
  transform: translateX(-50%);
  width: 30px; height: 48px; border-radius: 16px;
  /* Sep 3 2026 — the scrim came down and the footage came up, so the cue needs
     to carry its own contrast: a solid dark capsule with a gold rim rather than
     a faint outline that only read against a near-black wash. */
  border: 2px solid rgba(232, 190, 70, .75);
  background: rgba(14, 10, 5, .55); cursor: pointer; padding: 0;
  box-shadow: 0 2px 12px rgba(0, 0, 0, .5);
  transition: border-color .25s, background .25s;
}
.landing-scroll-cue:hover { border-color: var(--gold-bright, #f0cf7a); background: rgba(14, 10, 5, .78); }
.landing-scroll-cue span {
  display: block; width: 4px; height: 9px; border-radius: 2px; margin: 8px auto 0;
  background: #ffe9b4;
  animation: landing-cue 1.8s ease-in-out infinite;
}
@keyframes landing-cue {
  0%, 100% { transform: translateY(0); opacity: .95; }
  55%      { transform: translateY(16px); opacity: .2; }
}
@media (prefers-reduced-motion: reduce) {
  .landing-video { animation: none; transition: opacity .3s; }
  .landing-scroll-cue span { animation: none; }
}
@media (max-width: 760px) {
  .landing-reel-dots { right: 14px; bottom: 14px; }
  /* The cue used to be hidden here because the sign-in card sat inside the
     hero on a phone, so there was nothing below to point at. Since Sep 3 2026
     the card is a screen further down and the cue is the only thing that says
     so — it stays, one size smaller. */
  .landing-scroll-cue { width: 26px; height: 42px; bottom: 16px; }
  .landing-hero-cta { margin-top: 16px; }
}
@keyframes landing-drift {
  from { transform: scale(1.06) translate3d(-1.6%, 1%, 0); }
  to   { transform: scale(1.2) translate3d(1.6%, -1.2%, 0); }
}
/* Sep 3 2026 — "Make backscreenvideo less dark and more bright."
   The scrim was near-opaque at the left edge (.9) and half-covered at the right,
   which is what a two-column hero needed: the words sat on the dark side. With
   the hero down to an emblem and a title there is nothing to protect at the
   left, so the wash comes right down and the footage carries the page. The
   video itself is lifted a little as well, below. */
.landing-hero-scrim {
  position: absolute; inset: 0; z-index: 1;
  background: linear-gradient(100deg, rgba(14, 10, 5, .34) 0%, rgba(14, 10, 5, .22) 42%, rgba(14, 10, 5, .12) 100%);
}
/* Aug 12 2026 — no longer a two-column grid: the crest spans the full width and
   the two columns are a grid of their own underneath it (.landing-hero-row). */
.landing-hero-inner {
  position: relative; z-index: 2;
  width: min(1240px, 92vw); margin: 0 auto;
  padding: clamp(22px, 4.5vh, 48px) 0;
}
.landing-logo { width: 132px; height: 132px; object-fit: contain; filter: drop-shadow(0 6px 20px rgba(0, 0, 0, .7)); }
.landing-title {
  font-family: var(--serif); font-size: clamp(2.6rem, 6vw, 4.4rem); margin: 10px 0 0;
  letter-spacing: .05em; color: #f8eeda; text-shadow: 0 4px 22px rgba(0, 0, 0, .8);
}
.landing-sub {
  font-family: var(--serif); font-size: clamp(1rem, 2vw, 1.4rem);
  letter-spacing: .3em; text-transform: uppercase; color: var(--gold-bright);
  text-shadow: 0 2px 10px rgba(0, 0, 0, .8);
}
.landing-lede { font-size: 1.08rem; line-height: 1.65; max-width: 46ch; margin: 20px 0 16px; color: #ece0c9; }
.landing-points { list-style: none; padding: 0; margin: 0; display: flex; flex-direction: column; gap: 7px; }
.landing-points li { position: relative; padding-left: 24px; color: #d9cbb0; font-size: .96rem; }
.landing-points li::before {
  content: ''; position: absolute; left: 4px; top: .55em;
  width: 8px; height: 8px; border-radius: 50%; background: var(--gold);
}

.landing-card { padding: 26px 26px 20px; max-width: 430px; margin-left: auto; }
/* Aug 12 2026 — the line where the Odyssey briefing used to list the defenders.
   It says there is nothing to say, which is a thing the screen has to state
   outright: a briefing that simply omits the enemy reads as a bug. */
.mission-brief-blind { margin-top: 8px; font-style: italic; line-height: 1.5; }

.landing-standalone { max-width: 520px; text-align: center; margin: 0 auto; }
/* The standalone cards — confirm-your-email, the reset form, the link-expired
   page — are carved wood, but they carry `pub-light`, whose `.muted` is the
   page's dark ink. On this card that renders grey text on dark wood, i.e.
   nothing. Every one of these has shipped illegible at some point; naming the
   ink here fixes the whole family at once rather than one card at a time.

   Scoped through `.pub-light` rather than on `.landing-standalone` alone:
   `.pub-light .muted` further down the file is the same two-class weight and
   wins on source order, which is how the first attempt at this changed
   nothing. */
.pub-light .landing-standalone .muted,
.pub-light .landing-standalone p { color: #ddd0b4; text-shadow: 0 1px 4px rgba(0, 0, 0, .8); }
.pub-light .landing-standalone .landing-card-title { color: #fff3d4; }
.pub-light .landing-standalone b { color: #f6ecd2; }

/* Aug 12 2026 — the "confirm your address before you enter" room. It is a
   place the player acts in, not an error page, so the three ways out get equal
   weight and the message line has somewhere to appear without the card
   jumping. */
.verify-addr { margin: 12px 0 4px; }
.verify-note { font-size: .86rem; margin-bottom: 12px; }
.verify-actions { display: flex; gap: 8px; justify-content: center; flex-wrap: wrap; margin-top: 8px; }
.verify-msg { min-height: 20px; margin: 10px 0 0; }
.verify-msg.ok-msg { color: #2f7d43; }
.verify-change { margin-top: 16px; text-align: left; }
.verify-change[hidden] { display: none; }
.landing-card-title { font-family: var(--serif); margin: 0 0 10px; }
.landing-tabs { display: flex; gap: 6px; margin-bottom: 18px; }
.landing-tab {
  flex: 1; padding: 9px 10px; border-radius: 8px; cursor: pointer;
  font-family: var(--serif); font-size: .98rem;
  border: 1px solid var(--outline-var); background: rgba(255, 255, 255, .35); color: var(--on-surface-var);
}
.landing-tab.on { background: var(--primary); color: #fff; border-color: var(--primary); }
.landing-form { display: flex; flex-direction: column; gap: 12px; }
.landing-field { display: flex; flex-direction: column; gap: 5px; }
/* `display:flex` above beats the browser's default `[hidden] { display: none }`,
   so the optional email row stayed visible on the sign-in tab without this. */
.landing-field[hidden] { display: none; }
.landing-field > span {
  font-size: .72rem; letter-spacing: .1em; text-transform: uppercase; color: var(--on-surface-var);
}
.landing-field > span i { text-transform: none; letter-spacing: 0; font-style: normal; }
/* Aug 12 2026 — shown under the phone field when the site is invite-only, so a
   stranger learns that before filling in eight fields rather than after.
   The ink is named outright rather than taken from --on-surface-var: the card
   is dark wood, and the variable resolves to page ink here, which is how the
   note first shipped invisible. */
.landing-field-note {
  font-size: .78rem; font-style: normal; line-height: 1.45;
  color: #ddd0b4; text-shadow: 0 1px 4px rgba(0, 0, 0, .8);
}
.landing-field input {
  padding: 10px 12px; border-radius: 8px; border: 1px solid var(--outline-var);
  background: rgba(255, 253, 245, .92); font: inherit; color: var(--on-surface);
}
.landing-field input:focus { outline: none; border-color: var(--gold); box-shadow: 0 0 0 3px rgba(212, 160, 23, .22); }
.landing-err { color: var(--error); font-size: .86rem; min-height: 18px; }
.landing-go { width: 100%; justify-content: center; }
.landing-links { display: flex; justify-content: center; }
.linklike {
  background: none; border: none; padding: 4px; cursor: pointer;
  color: var(--secondary); font: inherit; font-size: .88rem; text-decoration: underline;
}
.landing-terms { font-size: .78rem; text-align: center; margin: 4px 0 0; }
.landing-terms a { color: var(--secondary); }
.landing-done { text-align: center; display: flex; flex-direction: column; gap: 12px; align-items: center; }

.landing-features { width: min(1240px, 92vw); margin: 0 auto; padding: 70px 0 40px; }
/* Aug 11 2026 — inside a frame that already supplies its own top and bottom,
   seventy pixels of section padding is an empty hand's width of parchment. */
.landing-reeled.landing-light .landing-features { padding-top: 10px; padding-bottom: 6px; }
.landing-feature {
  display: grid; grid-template-columns: 1fr 1fr; gap: 42px; align-items: center;
  padding: 34px 0; border-top: 1px solid rgba(212, 160, 23, .22);
}
.landing-feature:first-child { border-top: none; }
.landing-feature.is-flipped .landing-feature-art { order: 2; }
.landing-feature-text h2 {
  font-family: var(--serif); font-size: clamp(1.5rem, 3vw, 2.1rem); margin: 0 0 12px; color: #f6ebd6;
}
.landing-feature-text p { color: #cfc1a6; line-height: 1.7; margin: 0; max-width: 54ch; }
.landing-feature-art { display: flex; align-items: center; justify-content: center; }
.landing-art-scene img {
  width: 100%; max-height: 300px; object-fit: cover; border-radius: 12px;
  border: 1px solid rgba(212, 160, 23, .3); box-shadow: 0 14px 40px rgba(0, 0, 0, .5);
}
.landing-art-sprite img { max-width: 76%; max-height: 300px; object-fit: contain; filter: drop-shadow(0 18px 26px rgba(0, 0, 0, .6)); }

.landing-footer { border-top: 1px solid rgba(212, 160, 23, .25); background: rgba(0, 0, 0, .35); }
.landing-footer-inner {
  width: min(1240px, 92vw); margin: 0 auto; padding: 26px 0;
  display: flex; justify-content: space-between; align-items: center; gap: 20px; flex-wrap: wrap;
}
.landing-footer .muted { color: #a2937a; }
.landing-legal-links { display: flex; gap: 18px; flex-wrap: wrap; }
.landing-legal-links a { color: #d9cbb0; text-decoration: none; font-size: .92rem; }
.landing-legal-links a:hover { color: var(--gold-bright); text-decoration: underline; }

/* Card scheme marks. Each sits on its own pale chip so the artwork can be used
   unmodified: Visa is navy and the mastercard wordmark is black, and the footer
   is dark over the hero. The chip is the same in both footer states, which also
   stops the row moving when the page scrolls into the light theme. */
.landing-pay { display: flex; align-items: center; gap: 10px; }
.pay-mark {
  display: inline-flex; align-items: center; justify-content: center;
  width: 54px; height: 34px; padding: 5px 7px;
  background: #fbfaf6;
  border: 1px solid rgba(36, 26, 14, .16);
  border-radius: 6px;
  box-shadow: 0 1px 3px rgba(0, 0, 0, .22);
}
/* Constrained on BOTH axes, so the two very different aspect ratios (Visa is
   3:1, the Mastercard lockup roughly 1.3:1) end up optically the same weight
   instead of one filling the chip and the other floating in it. */
.pay-mark img { max-width: 100%; max-height: 22px; width: auto; height: auto; display: block; }
@media (max-width: 720px) {
  .landing-pay { width: 100%; }
}

/* Legal pages — long text, so they get a light sheet rather than the dark hero. */
.legal-page { width: min(760px, 92vw); padding: 34px 38px 30px; }
.legal-nav {
  display: flex; justify-content: space-between; gap: 16px; flex-wrap: wrap;
  padding-bottom: 14px; margin-bottom: 18px; border-bottom: 1px solid var(--outline-var);
}
.legal-nav a { color: var(--secondary); text-decoration: none; margin-left: 14px; }
.legal-nav a:first-child { margin-left: 0; }
.legal-nav a.on { color: var(--primary); font-weight: 700; }
.legal-page h1 { font-family: var(--serif); margin: 0 0 16px; }
.legal-body h2 { font-family: var(--serif); font-size: 1.15rem; margin: 22px 0 8px; }
.legal-body p { line-height: 1.7; margin: 0 0 12px; }
.legal-foot {
  margin-top: 26px; padding-top: 14px; border-top: 1px solid var(--outline-var); font-size: .85rem;
  /* Sep 3 2026 — the card marks joined this footer, so it is a row: identity on
     the left, marks on the right, wrapping to two lines when there is no room. */
  display: flex; align-items: center; justify-content: space-between; gap: 14px; flex-wrap: wrap;
}
.legal-foot .landing-pay { margin-left: auto; }

@media (max-width: 900px) {
  .landing-hero-inner { grid-template-columns: 1fr; gap: 28px; padding: 90px 0 50px; }
  .landing-card { margin: 0 auto; }
  .landing-feature { grid-template-columns: 1fr; gap: 20px; }
  .landing-feature.is-flipped .landing-feature-art { order: 0; }
  .landing-hero { min-height: auto; }
}
@media (prefers-reduced-motion: reduce) {
  .landing-video { animation: none; transform: scale(1.06); }
}

/* ── Public pages: Guide / FAQ / News + landing nav (Aug 5 2026) ─────────── */
/* The landing's public nav. It used to be absolutely positioned over the hero;
   since the landing gained the same masthead as the other public pages it is an
   ordinary flex child of .pub-head-inner, and positioning it took it out of
   that row — misaligned with the brand and 5px taller than the bar it sat in.
   The class stays as a marker; .pub-nav does the styling. */
.landing-topnav { gap: 26px; }

.landing-news-strip {
  display: flex; align-items: center; gap: 14px;
  width: min(1240px, 92vw); margin: 26px auto 0; padding: 13px 18px;
  border: 1px solid rgba(212, 160, 23, .35); border-radius: 10px;
  background: rgba(212, 160, 23, .08); color: #f0e4cb; text-decoration: none;
}
.landing-news-strip:hover { background: rgba(212, 160, 23, .14); }
.landing-news-tag {
  padding: 2px 10px; border-radius: 999px; background: var(--gold);
  color: #241a05; font-size: .72rem; letter-spacing: .12em; text-transform: uppercase; font-weight: 700;
}
.landing-news-strip b { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.landing-news-more { color: var(--gold-bright); white-space: nowrap; font-size: .9rem; }

.pub-head { border-bottom: 1px solid rgba(212, 160, 23, .25); background: rgba(0, 0, 0, .35); }
.pub-head-inner {
  width: min(1100px, 92vw); margin: 0 auto; padding: 14px 0;
  display: flex; align-items: center; gap: 26px;
}
.pub-brand { display: flex; align-items: center; gap: 10px; text-decoration: none; }
.pub-brand img { width: 44px; height: 44px; object-fit: cover; border-radius: 50%; }
.pub-brand span { font-family: var(--serif); font-size: 1.15rem; font-weight: 700; color: #f6ecd8; letter-spacing: .06em; }
.pub-nav { display: flex; gap: 20px; flex: 1; }
.pub-nav a { color: #d9cbb0; text-decoration: none; font-size: .95rem; padding: 6px 2px; }
.pub-nav a:hover { color: var(--gold-bright); }
.pub-nav a.on { color: var(--gold-bright); border-bottom: 2px solid var(--gold); }
.pub-play { white-space: nowrap; }

.pub-body { width: min(1100px, 92vw); margin: 0 auto; padding: 44px 0 70px; min-height: 55vh; }
.pub-title { font-family: var(--serif); font-size: clamp(1.9rem, 4.5vw, 3rem); margin: 0 0 8px; color: #f8eeda; }
.pub-lede { color: #cfc1a6; font-size: 1.05rem; margin: 0 0 26px; max-width: 68ch; }
.pub-section { margin: 34px 0; }
.pub-section h2 { font-family: var(--serif); font-size: 1.5rem; color: #f2e6ce; margin: 0 0 10px; }
.pub-section p { color: #cfc1a6; line-height: 1.7; max-width: 76ch; }
.pub-section b { color: #ecdfc4; }

.pub-resources { display: flex; gap: 12px; flex-wrap: wrap; margin: 16px 0; }
.pub-resource {
  display: flex; align-items: center; gap: 8px; padding: 8px 14px;
  border: 1px solid rgba(212, 160, 23, .3); border-radius: 999px; background: rgba(255, 255, 255, .04);
}
.pub-resource img { width: 26px; height: 26px; }
.pub-resource b { color: #f0e4cb; }
.pub-leagues { display: flex; gap: 12px; flex-wrap: wrap; margin: 14px 0; }
.pub-league {
  display: flex; align-items: center; gap: 8px; padding: 9px 15px;
  border: 1px solid rgba(212, 160, 23, .3); border-radius: 10px; background: rgba(255, 255, 255, .04);
}
.pub-league b { color: #f0e4cb; }
.pub-league-icon { font-size: 1.3rem; }

.pub-table-scroll { overflow-x: auto; margin: 14px 0; }
.pub-table { width: 100%; border-collapse: collapse; font-size: .92rem; color: #d9cbb0; }
.pub-table th {
  text-align: left; padding: 9px 10px; border-bottom: 2px solid rgba(212, 160, 23, .4);
  color: var(--gold-bright); font-size: .72rem; letter-spacing: .1em; text-transform: uppercase;
}
.pub-table td { padding: 8px 10px; border-bottom: 1px solid rgba(212, 160, 23, .14); }
.pub-table td.num, .pub-table th.num { text-align: right; font-variant-numeric: tabular-nums; }
.pub-table b { color: #f0e4cb; }
.pub-unit-icon { width: 40px; height: 40px; object-fit: contain; filter: drop-shadow(0 2px 4px rgba(0,0,0,.6)); }
.pub-tag {
  font-size: .66rem; letter-spacing: .08em; text-transform: uppercase;
  border: 1px solid rgba(212, 160, 23, .5); border-radius: 4px; padding: 1px 5px; color: var(--gold-bright);
}

.pub-legends { display: grid; grid-template-columns: repeat(auto-fill, minmax(190px, 1fr)); gap: 14px; margin: 16px 0; }
.pub-legend {
  display: flex; flex-direction: column; align-items: center; gap: 6px; text-align: center;
  padding: 16px 12px; border: 1px solid rgba(212, 160, 23, .25); border-radius: 12px;
  background: rgba(255, 255, 255, .03);
}
.pub-legend img { width: 84px; height: 84px; object-fit: contain; filter: drop-shadow(0 8px 14px rgba(0,0,0,.6)); }
.pub-legend b { color: #f0e4cb; font-family: var(--serif); }
.pub-legend-stats { color: var(--gold-bright); font-size: .8rem; }
.pub-legend .muted { font-size: .82rem; color: #a2937a; }

.pub-tiers { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 10px; margin: 16px 0; }
.pub-tier {
  display: flex; flex-direction: column; gap: 2px; padding: 11px 13px;
  border: 1px solid rgba(212, 160, 23, .25); border-left-width: 4px; border-radius: 8px;
  background: rgba(255, 255, 255, .03); color: #d9cbb0; font-size: .86rem;
}
.pub-tier b { color: #f0e4cb; }
.pub-tier[data-mult="0.5"], .pub-tier[data-mult="0.7"] { border-left-color: #4d8f5c; }
.pub-tier[data-mult="0.9"], .pub-tier[data-mult="1"] { border-left-color: var(--gold); }
.pub-tier[data-mult="1.2"], .pub-tier[data-mult="1.5"] { border-left-color: #c07a3a; }
.pub-tier[data-mult="2"] { border-left-color: #b0453a; }

.pub-cta { margin: 34px 0 6px; display: flex; justify-content: center; }

.pub-faq { display: flex; flex-direction: column; gap: 10px; max-width: 76ch; }
.pub-faq-item {
  border: 1px solid rgba(212, 160, 23, .25); border-radius: 10px;
  background: rgba(255, 255, 255, .03); overflow: hidden;
}
.pub-faq-item summary {
  cursor: pointer; padding: 15px 18px; font-family: var(--serif); font-size: 1.05rem; color: #f0e4cb;
  list-style: none; position: relative; padding-right: 40px;
}
.pub-faq-item summary::-webkit-details-marker { display: none; }
.pub-faq-item summary::after {
  content: '+'; position: absolute; right: 18px; top: 50%; transform: translateY(-50%);
  color: var(--gold-bright); font-size: 1.3rem;
}
.pub-faq-item[open] summary::after { content: '–'; }
.pub-faq-item p { margin: 0; padding: 0 18px 16px; color: #cfc1a6; line-height: 1.7; }

.pub-news { max-width: 76ch; padding: 22px 0; border-bottom: 1px solid rgba(212, 160, 23, .18); }
.pub-news:last-of-type { border-bottom: none; }
.pub-news header { display: flex; justify-content: space-between; align-items: baseline; gap: 16px; flex-wrap: wrap; }
.pub-news h2 { font-family: var(--serif); font-size: 1.4rem; color: #f2e6ce; margin: 0; }
.pub-news-body { color: #cfc1a6; line-height: 1.7; }
.pub-news-body h3 { font-family: var(--serif); color: #ecdfc4; margin: 16px 0 6px; }
.pub-news-body p { margin: 8px 0; }

/* Admin: user dossier */
.admin-dossier { min-width: min(760px, 86vw); max-height: 74vh; overflow-y: auto; }
.admin-dossier-head { margin-bottom: 14px; }
.admin-dossier-head h2 { margin: 0; }
.admin-dossier-cols { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; }
.admin-dossier-cols h3 {
  margin: 14px 0 6px; font-size: .78rem; letter-spacing: .1em;
  text-transform: uppercase; color: var(--on-surface-var);
}
.admin-dossier-cols h3:first-child { margin-top: 0; }
.admin-dossier-block { font-size: .9rem; display: flex; flex-direction: column; gap: 5px; }
.admin-dossier-row { display: flex; justify-content: space-between; gap: 12px; }
.admin-win { color: #1e5c31; font-weight: 700; }
.admin-loss { color: #8f2318; font-weight: 700; }
.admin-dossier-polis {
  border: 1px solid var(--outline-var); border-radius: 8px; margin-bottom: 8px;
  background: var(--surface-low);
}
.admin-dossier-polis summary { cursor: pointer; padding: 9px 12px; }
.admin-dossier-sub { padding: 4px 12px 11px; display: flex; flex-direction: column; gap: 6px; font-size: .88rem; }
.admin-dossier-sub .label-caps { margin-right: 6px; }

@media (max-width: 800px) {
  .admin-dossier-cols { grid-template-columns: 1fr; }
  .pub-head-inner { flex-wrap: wrap; gap: 12px; }
  .pub-nav { order: 3; width: 100%; }
}

/* ── Light variants (Aug 6 2026) ────────────────────────────────────────────
   "Leave the header dark, but make the background white." Two scopes:
   .pub-light  — legal pages: dark masthead over a white reading sheet.
   .landing-light — the landing: dark masthead + dark hero, white below.
   ───────────────────────────────────────────────────────────────────────── */
.landing.pub-light { background: #fdfcf6; color: var(--on-surface); }
.pub-light .pub-head, .landing-light .pub-head {
  /* Solid, not translucent: the see-through black works over a dark page but
     turns muddy grey the moment there is white behind it. */
  background: #171106; border-bottom: 2px solid var(--gold);
}
.pub-light .pub-title { color: var(--on-surface); }
.pub-light .legal-body { color: #4a3d2c; max-width: 76ch; }
.pub-light .legal-body h2 { font-family: var(--serif); font-size: 1.2rem; color: #241a0e; margin: 24px 0 8px; }
.pub-light .legal-body p { line-height: 1.75; margin: 0 0 13px; }
.pub-light .legal-foot { border-top: 1px solid var(--outline-var); }
.pub-light .pub-body { min-height: 60vh; }

/* Aug 11 2026 — "Make so on each page there is a background photo." The reading
   pages painted a full-bleed opaque sheet, which covered the scene body now
   carries behind them (see .has-bare-bg and PAGE_BGS). The sheet becomes a
   floating panel instead: the photograph shows around and through it, and the
   text keeps a solid enough backing to stay readable on any of them — measured
   against the darkest (the forge) and the brightest (the gymnasion). */
body.has-bare-bg .landing.pub-light,
body.has-bare-bg .landing.landing-light { background: none; }
body.has-bare-bg .pub-light .pub-body {
  background: rgba(253, 250, 242, .93);
  border: 1px solid rgba(120, 92, 48, .3);
  border-radius: 10px;
  box-shadow: 0 18px 50px rgba(20, 14, 6, .45);
  padding: 40px 44px 56px;
  margin: 38px auto 60px;
  backdrop-filter: blur(2px);
}
/* Aug 12 2026 — "work on the background, as it is all in 1 block doesn't look
   good."
   
   The guide is four chapters and several thousand words; one unbroken cream
   slab from the masthead to the footer flattened all of it into a single
   object and hid the painted background the rest of the site is built on.
   
   The page carries NO sheet of its own here. Each chapter gets one instead —
   its own panel, its own shadow, gaps between them where the background shows
   through — so the page reads as four things laid on a table rather than one
   long document. The rules chapter is deliberately the widest, because it is
   the one people actually read.
   
   `backdrop-filter` moves with the sheet: it is what makes an element the
   containing block for `position: fixed` descendants, and the chapter rail is
   fixed. It stays off the wrapper for exactly that reason. */
body.has-bare-bg .pub-light .pub-body:has(.guide-chapter) {
  background: none; border: none; box-shadow: none; backdrop-filter: none;
  padding: 40px 0 56px; border-radius: 0;
}
body.has-bare-bg .pub-light .pub-body:has(.guide-chapter) > .pub-title,
body.has-bare-bg .pub-light .pub-body:has(.guide-chapter) > .pub-lede {
  /* The page title sits on the background rather than on paper, so it takes
     the light-on-dark treatment the landing hero uses. */
  color: #f8eeda; text-shadow: 0 3px 16px rgba(0, 0, 0, .85);
  width: min(1100px, 92vw); margin-left: auto; margin-right: auto;
}
body.has-bare-bg .pub-light .pub-body:has(.guide-chapter) > .pub-lede {
  color: #e2d5b8; text-shadow: 0 2px 10px rgba(0, 0, 0, .8);
}
.guide-chapter-sheet {
  background: rgba(253, 250, 242, .95);
  border: 1px solid rgba(120, 92, 48, .3);
  border-radius: 12px;
  box-shadow: 0 18px 46px rgba(20, 14, 6, .45);
  padding: 26px 38px 34px;
  margin: 0 auto;
  width: min(1100px, 92vw);
}
/* Aug 12 2026 — "The four things you do - remove background white frame."
   A chapter made entirely of wooden boards and parchment scrolls carries its
   own materials; a paper sheet under them is a second background arguing with
   the first. Chapters I and II take nothing. */
.guide-chapter-bare > .guide-chapter-sheet {
  background: none; border: none; box-shadow: none; padding: 0;
}
.guide-chapter + .guide-chapter { margin-top: 34px; }
/* The chapter's own plank hangs ABOVE its sheet, on the background, which is
   what makes each chapter read as a titled object. */
.guide-chapter > .guide-chapter-head { width: min(1100px, 92vw); margin-left: auto; margin-right: auto; }
@media (max-width: 720px) {
  .guide-chapter-sheet { padding: 20px 18px 26px; border-radius: 8px; }
}
body.has-bare-bg .pub-light .landing-footer,
body.has-bare-bg .pub-light .legal-foot { background: none; border-top: none; }
body.has-bare-bg .pub-light .landing-footer-inner {
  background: rgba(23, 17, 6, .78); color: #efe4c8;
  border-radius: 10px; padding: 20px 24px;
}
body.has-bare-bg .pub-light .landing-footer-inner b { color: #fff3d4; }
body.has-bare-bg .pub-light .landing-footer-inner .muted,
body.has-bare-bg .pub-light .landing-legal-links a { color: #cbbb96; }
@media (max-width: 720px) {
  body.has-bare-bg .pub-light .pub-body { padding: 26px 20px 36px; margin: 20px auto 34px; }
}

.landing.landing-light { background: #fdfcf6; }
/* Aug 11 2026 — with the reel fixed behind the whole page, the cream sheet the
   light theme painted over everything is exactly what must not be there. The
   page has no background of its own now; the reading sections carry their own,
   translucent, so the footage moves around and faintly through them while the
   words stay as legible as they were on a solid page. The footer keeps its
   opaque one, which is what gives the scroll somewhere to arrive. */
.landing.landing-light.landing-reeled { background: none; }
/* Aug 12 2026 — "Instead paper background use scroll and wooden frames for each
   seperate segment."

   One sheet of papyrus used to hold all five feature bands at once, divided by
   hairlines — so five separate claims about the game read as one long block of
   parchment. Each band is now its own carved board: frame_wood.png over
   wood_panel.jpg, at the exact 9-slice scale measured for the Progress window
   (uniform k per side, `round` so the meander runs as whole repeats rather than
   smeared to fit). The container carries nothing at all. */
.landing-reeled.landing-light .landing-features {
  width: min(1160px, 92vw); margin-left: auto; margin-right: auto;
  border: none; background: none; padding: 0;
}
.landing-wood {
  border: solid transparent;
  border-width: 31px 52px 32px 53px;
  border-image-source: url('/assets/ui/frame_wood.png?v=49');
  border-image-slice: 68 116 70 118;
  border-image-width: 31px 52px 32px 53px;
  border-image-repeat: round round;
  /* The planks come from their own texture, not the frame's fill slice:
     `round` tiles the fill along with the edges and would seam the middle. */
  background: url('/assets/ui/wood_panel.jpg?v=49') center / cover no-repeat #3d2a19;
  background-clip: padding-box;
  border-radius: 0;
  filter: drop-shadow(0 16px 38px rgba(16, 11, 4, .5));
}
@media (max-width: 720px) {
  .landing-wood {
    border-width: 18px 30px 19px 31px;
    border-image-width: 18px 30px 19px 31px;
  }
}

/* The closing plank is the second scroll on the page — narrow, so it takes the
   .k-scroll-sm metrics rather than the full 128/118 shoulders. */
.landing-reeled.landing-light .landing-cta {
  width: min(860px, 92vw); margin-left: auto; margin-right: auto;
  border: solid transparent;
  border-width: 88px 74px 90px 74px;
  border-image: url('/assets/ui/scroll_frame.png?v=49') 175 160 175 160 fill / 88px 74px 90px 74px stretch;
  background: none;
  border-radius: 0;
  filter: drop-shadow(0 18px 44px rgba(16, 11, 4, .5));
  padding: 0;
  max-width: none;
  margin-bottom: 30px;
}
.landing-cta-inner { padding: 0 30px; }

/* ── the FAQ at the foot of the landing page ──────────────────────────────
   Aug 12 2026 — "Put FAQ on the main mage on the buttom and make it pretty
   (like on the scroll that then becomes becomes bigger)."

   Each question is a rolled scroll — the frame's own rods, with a hand's width
   of parchment between them. Opening it unrolls: the rods move apart, the sheet
   grows to the height of its answer and the answer comes down inside. The
   growth is a real measured pixel height rather than a guessed maximum, so a
   two-line answer opens to two lines and a six-line one to six. */
.landing-faq { width: min(1000px, 92vw); margin: 0 auto 30px; }
.landing-faq-head { text-align: center; margin-bottom: 22px; }
.landing-faq-head h2 {
  font-family: var(--serif); font-size: clamp(1.7rem, 3.4vw, 2.6rem);
  color: #f8eeda; text-shadow: 0 3px 16px rgba(0, 0, 0, .85); margin: 0 0 6px;
}
.landing-faq-head p { color: #e2d5b8; text-shadow: 0 2px 8px rgba(0, 0, 0, .8); margin: 0; }
.landing-faq-list { display: flex; flex-direction: column; gap: 12px; }
.lfaq {
  border: solid transparent;
  border-width: 52px 46px 54px 46px;
  border-image: url('/assets/ui/scroll_frame.png?v=49') 175 160 175 160 fill / 52px 46px 54px 46px stretch;
  background: none; padding: 0;
  filter: drop-shadow(0 10px 26px rgba(16, 11, 4, .45));
  transition: border-image-width .38s cubic-bezier(.22, 1, .36, 1),
              border-width .38s cubic-bezier(.22, 1, .36, 1),
              filter .3s;
}
.lfaq.is-open {
  border-width: 66px 52px 68px 52px;
  border-image-width: 66px 52px 68px 52px;
  filter: drop-shadow(0 16px 38px rgba(16, 11, 4, .55));
}
/* Aug 12 2026 — "make that the topic is in the middle." The question sat hard
   against the left rod with the chevron pushed to the right one; it reads as a
   title on a scroll now, centred, with the mark travelling beside it rather
   than pinned to the far edge. */
.lfaq-q {
  display: flex; align-items: center; justify-content: center; gap: 14px;
  width: 100%; padding: 0 12px; margin: 0; cursor: pointer;
  background: none; border: none; text-align: center;
  font-family: var(--serif); font-size: clamp(1.02rem, 1.6vw, 1.24rem);
  color: #2a2013; line-height: 1.35;
}
.lfaq-q:hover { color: #7a520f; }
.lfaq-q:focus-visible { outline: 2px solid var(--gold); outline-offset: 4px; border-radius: 4px; }
/* the mark: a plain chevron that turns, drawn rather than typed */
.lfaq-mark {
  flex: none; width: 13px; height: 13px; border-right: 2px solid #9b7433;
  border-bottom: 2px solid #9b7433; transform: rotate(45deg) translate(-3px, -3px);
  transition: transform .3s cubic-bezier(.22, 1, .36, 1), border-color .2s;
}
.lfaq.is-open .lfaq-mark { transform: rotate(225deg) translate(-3px, -3px); border-color: #7a520f; }
.lfaq-a {
  max-height: 0; overflow: hidden; opacity: 0;
  transition: max-height .38s cubic-bezier(.22, 1, .36, 1), opacity .28s ease;
}
.lfaq.is-open .lfaq-a { opacity: 1; }
/* The ANSWER stays left-aligned: centred prose is unreadable past one line. */
.lfaq-a p { margin: 12px auto 2px; max-width: 68ch; color: #4a3a24; line-height: 1.7; font-size: .97rem; }
.landing-faq-more { display: flex; justify-content: center; gap: 26px; flex-wrap: wrap; margin-top: 20px; }
.landing-faq-more a {
  color: #f0cf7a; text-decoration: none; font-size: .96rem;
  text-shadow: 0 2px 8px rgba(0, 0, 0, .85);
}
.landing-faq-more a:hover { color: #fff6d6; text-decoration: underline; }
@media (max-width: 720px) {
  .lfaq { border-width: 40px 30px 42px 30px; border-image-width: 40px 30px 42px 30px; }
  .lfaq.is-open { border-width: 50px 34px 52px 34px; border-image-width: 50px 34px 52px 34px; }
  .lfaq-q { padding: 0 2px; }
  .lfaq-a p { margin-left: 2px; margin-right: 2px; }
}
@media (prefers-reduced-motion: reduce) {
  .lfaq, .lfaq-a, .lfaq-mark { transition: none; }
}
.landing-reeled .landing-news-strip { box-shadow: 0 14px 34px rgba(16, 11, 4, .45); }
@media (max-width: 720px) {
  .landing-reeled.landing-light .landing-cta {
    border-width: 62px 40px 64px 40px;
    border-image-width: 62px 40px 64px 40px;
  }
  .landing-cta-inner { padding: 0 8px; }
}
.landing-light .landing-hero { min-height: calc(100vh - 74px); }
.landing-light .landing-feature-text h2 { color: #241a0e; }
.landing-light .landing-feature-text p { color: #554336; }
.landing-light .landing-feature { border-top: 1px solid #e7dcc4; }
/* Aug 12 2026 — "make so image is in the wooden frame, when text is in scroll."
   The band itself is bare: the picture takes the carved wood, the words take
   the parchment, and the reel runs between the two. The hairline that used to
   divide one band from the next has nothing left to divide. */
.landing-light .landing-feature,
.landing-reeled.landing-light .landing-feature { border-top: none; background: none; }
.landing-reeled.landing-light .landing-feature { padding: 0; align-items: center; }
.landing-reeled.landing-light .landing-feature + .landing-feature { margin-top: 34px; }
/* The wood goes round the CAPTURE, so the frame has to hug it — no interior
   padding, and the planks show only in the sliver the carving leaves. */
.landing-feature-art.landing-wood { display: block; padding: 0; }
.landing-feature-art.landing-wood img { width: 100%; display: block; }
/* The words hang on the scroll at its small scale — a half-width column is
   560-odd pixels, which is what .k-scroll-sm was measured for. */
.landing-reeled.landing-light .landing-feature-text {
  border: solid transparent;
  border-width: 88px 74px 90px 74px;
  border-image: url('/assets/ui/scroll_frame.png?v=49') 175 160 175 160 fill / 88px 74px 90px 74px stretch;
  background: none; padding: 0;
  filter: drop-shadow(0 14px 34px rgba(16, 11, 4, .5));
}
/* The torn parchment edge wanders further in than the border is drawn. */
.landing-feature-text-inner { padding: 0 22px; }
.landing-light .landing-feature-text h2 { color: #241a0e; }
.landing-light .landing-feature-text p { color: #4a3a24; }
@media (max-width: 900px) {
  .landing-reeled.landing-light .landing-feature-text {
    border-width: 66px 44px 68px 44px;
    border-image-width: 66px 44px 68px 44px;
  }
  .landing-feature-text-inner { padding: 0 8px; }
}
.landing-light .landing-art-scene img { border-color: #e0d0a8; box-shadow: 0 12px 32px rgba(60, 44, 18, .18); }
.landing-light .landing-art-sprite img { filter: drop-shadow(0 14px 20px rgba(60, 44, 18, .3)); }
.landing-light .landing-news-strip {
  background: #fdf6e0; border-color: #e2c66e; color: #241a0e;
}
.landing-light .landing-news-strip:hover { background: #faefcd; }
.landing-light .landing-news-more { color: var(--primary); }
/* Aug 12 2026 — the landing page's footer was still a flat cream slab, left
   over from when this page WAS a light sheet. Since the reskin it is painted
   scenery from the masthead to the closing scroll, and the footer was the one
   element that looked like a different website bolted to the bottom. The
   guide, FAQ and news pages already got this treatment (see the
   `body.has-bare-bg .pub-light` block); the homepage never did, because it
   carries `.landing-light` rather than `.pub-light`.
   The legal pages keep the cream slab on purpose — those genuinely are light
   sheets, and a dark strip under them would be the same mistake reversed.

   Scoped `.landing-light.landing-reeled` rather than `.landing-reeled` alone:
   the light rules below are the same two-class weight and are defined later in
   the file, so a shorter selector here loses on source order — which is exactly
   how this first shipped not working. Three classes wins outright. */
.landing-light.landing-reeled .landing-footer { background: none; border-top: none; }
.landing-light.landing-reeled .landing-footer-inner {
  background: rgba(23, 17, 6, .78); color: #efe4c8;
  border-radius: 10px; padding: 20px 24px; margin-bottom: 26px;
}
.landing-light.landing-reeled .landing-footer-inner b,
.landing-light.landing-reeled .landing-footer .site-footer-line { color: #fff3d4; }
.landing-light.landing-reeled .landing-footer .muted,
.landing-light.landing-reeled .landing-footer .muted.site-footer-line,
.landing-light.landing-reeled .landing-legal-links a { color: #cbbb96; }
.landing-light.landing-reeled .landing-legal-links a:hover { color: var(--gold-bright); }

.landing-light .landing-footer { background: #f3f0e4; border-top: 1px solid #e0d5bd; }
/* The container's own `color` was cream — the same cream as its background.
   Nothing inherits it today because every child names its own ink, but the
   next line of text added here would ship invisible. */
.landing-light .landing-footer { color: #241a0e; }
.landing-light .landing-footer b { color: #241a0e; }
/* The year-and-domain line is a div, not a <b> — so the rule above never
   reached it and it inherited the dark theme's near-white on the light
   theme's cream. Invisible, on every light page, since the day the light
   footer shipped. */
.landing-light .landing-footer .site-footer-line,
.pub-light .landing-footer .site-footer-line { color: #241a0e; }
.landing-light .landing-footer .muted.site-footer-line,
.pub-light .landing-footer .muted.site-footer-line { color: #7c6d55; }
.landing-light .landing-footer .muted { color: #7c6d55; }
.landing-light .landing-legal-links a { color: #554336; }
.landing-light .landing-legal-links a:hover { color: var(--primary); }

/* Guide / FAQ / News on the white sheet. The dark rules above stay as the base
   (they still describe the layout); these only repaint what a light ground
   changes — text colour, card fills, borders and the table's hairlines. */
.pub-light .pub-lede { color: #554336; }
.pub-light .pub-section h2 { color: #241a0e; }
.pub-light .pub-section p { color: #4a3d2c; }
.pub-light .pub-section b { color: #241a0e; }
.pub-light .muted { color: #7c6d55; }

.pub-light .pub-resource,
.pub-light .pub-league {
  border-color: var(--outline-var); background: var(--surface-low);
}
.pub-light .pub-resource b, .pub-light .pub-league b { color: #241a0e; }

.pub-light .pub-table { color: #4a3d2c; }
.pub-light .pub-table th { color: var(--primary); border-bottom-color: var(--gold); }
.pub-light .pub-table td { border-bottom-color: #eadfc8; }
.pub-light .pub-table b { color: #241a0e; }
.pub-light .pub-table tr:hover td { background: var(--surface-low); }
.pub-light .pub-tag { color: var(--primary); border-color: var(--gold); }
.pub-light .pub-unit-icon { filter: none; }

.pub-light .pub-legend {
  border-color: var(--outline-var); background: var(--surface-low);
}
.pub-light .pub-legend b { color: #241a0e; }
.pub-light .pub-legend-stats { color: var(--primary); }
.pub-light .pub-legend img { filter: drop-shadow(0 6px 12px rgba(60, 44, 18, .25)); }

.pub-light .pub-tier {
  border-color: var(--outline-var); background: var(--surface-low); color: #4a3d2c;
}
.pub-light .pub-tier b { color: #241a0e; }

.pub-light .pub-faq-item { border-color: var(--outline-var); background: var(--surface-low); }
.pub-light .pub-faq-item summary { color: #241a0e; }
.pub-light .pub-faq-item summary::after { color: var(--primary); }
.pub-light .pub-faq-item p { color: #4a3d2c; }

.pub-light .pub-news { border-bottom-color: #e7dcc4; }
.pub-light .pub-news h2 { color: #241a0e; }
.pub-light .pub-news-body { color: #4a3d2c; }
.pub-light .pub-news-body h3 { color: #241a0e; }

/* Footer, shared by every light public page. */
.pub-light .landing-footer { background: #f3f0e4; border-top: 1px solid #e0d5bd; }
.pub-light .landing-footer b { color: #241a0e; }
.pub-light .landing-footer .muted { color: #7c6d55; }
.pub-light .landing-legal-links a { color: #554336; }
.pub-light .landing-legal-links a:hover { color: var(--primary); }

/* ── How-to-play: a picture book (Aug 6 2026) ───────────────────────────────
   "Less technical information, rather visuals of what's happening." Each step
   is a screenshot of the real screen with a short caption beside it; the
   rosters are portraits with a one-line job, not stat blocks.
   ───────────────────────────────────────────────────────────────────────── */
/* Aug 12 2026 — "Use wooden frames, scrolls." The steps were a rounded
   rectangle beside a paragraph of bare text; they are the landing page's
   pairing now — the capture hung in the carved frame, the words on parchment
   beside it — so the two public pages are recognisably the same place. */
.guide-step {
  display: grid; grid-template-columns: 1fr 1fr; gap: 28px; align-items: center;
  padding: 22px 0; border-top: none;
}
.guide-step:first-of-type { padding-top: 8px; }
.guide-step:nth-child(even) .guide-step-text { order: 2; }
.guide-step-n {
  display: inline-flex; align-items: center; justify-content: center;
  width: 38px; height: 38px; border-radius: 50%; margin-bottom: 10px;
  background: var(--primary); color: #fff;
  font-family: var(--serif); font-size: 1.2rem; font-weight: 700;
}
.guide-step-text {
  border: solid transparent;
  border-width: 88px 74px 90px 74px;
  border-image: url('/assets/ui/scroll_frame.png?v=49') 175 160 175 160 fill / 88px 74px 90px 74px stretch;
  background: none; padding: 0;
  filter: drop-shadow(0 14px 32px rgba(60, 44, 18, .3));
}
/* The torn parchment edge wanders further in than the border is drawn. */
.guide-step-text-inner { padding: 0 22px; }
.guide-step-text h2 { font-family: var(--serif); font-size: clamp(1.3rem, 2.3vw, 1.8rem); margin: 0 0 10px; color: #241a0e; }
.guide-step-text p { color: #4a3a24; line-height: 1.7; margin: 0; font-size: .98rem; }
.guide-shot { margin: 0; }
.guide-shot img {
  /* border-box, or the frame's 105px of horizontal border is ADDED to a 100%
     width and the picture hangs out of its own column. */
  width: 100%; display: block; box-sizing: border-box; border-radius: 0;
  border: solid transparent;
  border-width: 31px 52px 32px 53px;
  border-image-source: url('/assets/ui/frame_wood.png?v=49');
  border-image-slice: 68 116 70 118;
  border-image-width: 31px 52px 32px 53px;
  border-image-repeat: round round;
  background: url('/assets/ui/wood_panel.jpg?v=49') center / cover no-repeat #3d2a19;
  background-clip: padding-box;
  box-shadow: none;
  filter: drop-shadow(0 14px 32px rgba(60, 44, 18, .32));
}
.guide-shot figcaption { margin-top: 10px; color: #7c6d55; font-size: .88rem; font-style: italic; text-align: center; }
@media (max-width: 1100px) {
  .guide-step-text {
    border-width: 66px 44px 68px 44px;
    border-image-width: 66px 44px 68px 44px;
  }
  .guide-step-text-inner { padding: 0 8px; }
  .guide-shot img {
    border-width: 18px 30px 19px 31px;
    border-image-width: 18px 30px 19px 31px;
  }
}

.guide-roster {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(148px, 1fr));
  gap: 14px; margin: 20px 0 6px;
}
.guide-unit {
  margin: 0; padding: 14px 10px; text-align: center;
  border: 1px solid var(--outline-var); border-radius: 12px; background: var(--surface-low);
  display: flex; flex-direction: column; align-items: center; gap: 8px;
}
.guide-unit img { width: 78px; height: 78px; object-fit: contain; }
.guide-unit figcaption { display: flex; flex-direction: column; gap: 3px; }
.guide-unit figcaption b { color: #241a0e; font-family: var(--serif); font-size: 1rem; }
.guide-unit figcaption span { color: #7c6d55; font-size: .82rem; line-height: 1.45; }
.guide-roster-legends { grid-template-columns: repeat(auto-fill, minmax(178px, 1fr)); }
.guide-unit-legend img { width: 96px; height: 96px; filter: drop-shadow(0 8px 14px rgba(60, 44, 18, .3)); }

.guide-duo { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; margin: 18px 0 6px; }
.guide-duo img {
  width: 100%; height: 220px; object-fit: cover; border-radius: 12px;
  border: 1px solid #e0d0a8; box-shadow: 0 12px 30px rgba(60, 44, 18, .18);
}

/* ─────────────────────────────────────────────────────────────────────────
   Aug 12 2026 — "Make how to play in different section and work on the
   structure so it looks more UI/UX firnddly. You can use characters so they
   explain it easier and appear fro the side of the screen to explain. Use
   wooden frames, scrolls, icons from the game instead of emojis."

   Four chapters, each headed by the game's own carved plank; a rail down the
   left that tracks which one you are in; the five screens paired wood-to-
   picture and scroll-to-words exactly as the landing page does; and the rules
   spoken by portraits that slide in from the edge of the screen.
   ───────────────────────────────────────────────────────────────────────── */
.guide-chapter { margin: 0 0 18px; scroll-margin-top: 80px; }
.guide-chapter-head { margin: 46px 0 10px; }
.guide-chapter:first-of-type .guide-chapter-head { margin-top: 8px; }
/* The plank is the game's own .sec-plank, so a chapter title here is the same
   carved object as a section title inside the game. */
.guide-plank {
  display: inline-flex; align-items: center; gap: 14px;
  padding-left: 8px; padding-right: 14px; min-width: min(520px, 90%);
}
.guide-chapter-n {
  font-family: var(--serif); font-size: 1.5rem; color: var(--gold-bright);
  text-shadow: 0 2px 4px rgba(0, 0, 0, .8); min-width: 34px; text-align: center;
}
.guide-chapter-title {
  font-family: var(--serif); font-size: clamp(1.2rem, 2.4vw, 1.7rem);
  color: #f6ecd2; text-shadow: 0 2px 5px rgba(0, 0, 0, .8);
}
/* On the background beneath the plank, so it takes light ink. */
.guide-chapter-lede {
  color: #e2d5b8; text-shadow: 0 2px 8px rgba(0, 0, 0, .8);
  margin: 12px 0 16px; max-width: 70ch; font-size: 1.02rem;
}

/* the rail — where you are, always visible, never in the way */
/* The rail hangs in the gutter OUTSIDE the page panel, which means it sits over
   the painted background rather than over paper — so it carries its own dark
   plate and light ink, the same treatment the footer uses out there. Reading
   it against an olive grove was the alternative, and it was not readable. */
.guide-rail {
  position: fixed; left: max(14px, calc((100vw - 1100px) / 2 - 200px)); top: 42%;
  transform: translateY(-50%); z-index: 4;
  display: flex; flex-direction: column; gap: 2px; width: 182px;
  padding: 10px 8px; border-radius: 10px;
  background: rgba(23, 17, 6, .82);
  border: 1px solid rgba(212, 160, 23, .28);
  box-shadow: 0 14px 34px rgba(12, 8, 3, .5);
  backdrop-filter: blur(2px);
}
.guide-rail-item {
  display: flex; align-items: center; gap: 10px; padding: 8px 10px;
  border-left: 2px solid rgba(212, 160, 23, .3); border-radius: 0 6px 6px 0;
  color: #cbbb96; text-decoration: none; font-size: .84rem; line-height: 1.3;
  transition: color .2s, border-color .2s, background .2s;
}
.guide-rail-item:hover { color: #f6ecd2; background: rgba(212, 160, 23, .14); }
.guide-rail-item.on { color: #f8eeda; border-left-color: var(--gold-bright); background: rgba(212, 160, 23, .2); }
.guide-rail-n { font-family: var(--serif); font-style: normal; font-size: 1rem; color: var(--gold-bright); min-width: 20px; }
/* Below 1500px there is no gutter left to hang it in. */
@media (max-width: 1500px) { .guide-rail { display: none; } }

/* ── the speakers ────────────────────────────────────────────────────────
   One of the game's medallion portraits per rule, alternating sides, arriving
   from its own edge as the section reaches the middle of the viewport. */
.guide-said {
  display: grid; grid-template-columns: 132px minmax(0, 1fr); gap: 26px;
  align-items: start;
  margin: 0 0 26px;
}
/* "put each category in seperate scroll frame" — every rule is its own sheet of
   parchment, with the speaker standing beside it rather than on it. */
.guide-said-scroll {
  border: solid transparent;
  border-width: 78px 64px 80px 64px;
  border-image: url('/assets/ui/scroll_frame.png?v=49') 175 160 175 160 fill / 78px 64px 80px 64px stretch;
  background: none; padding: 0;
  filter: drop-shadow(0 14px 34px rgba(20, 14, 6, .45));
  min-width: 0;
}
.guide-said-body { padding: 0 20px; min-width: 0; }
.guide-said-body h2 { font-family: var(--serif); font-size: 1.4rem; color: #241a0e; margin: 0 0 10px; }
.guide-said-body p { color: #4a3a24; line-height: 1.72; margin: 0 0 14px; max-width: 74ch; }
.guide-said-body p:last-child { margin-bottom: 0; }
.guide-said-body b { color: #241a0e; }
/* The speaker's name sits on the background between the scrolls now. */
/* `.pub-light .pub-section b` also matches this name — same two-class weight,
   defined later in the file — so the caption came out in page ink on a dark
   background and was unreadable. Scoped one class deeper to win outright. */
.guide-chapter-bare .guide-speaker .guide-speaker-name {
  color: #e2d5b8; text-shadow: 0 2px 8px rgba(0, 0, 0, .85);
}
@media (max-width: 900px) {
  .guide-said-scroll {
    border-width: 58px 40px 60px 40px;
    border-image-width: 58px 40px 60px 40px;
  }
  .guide-said-body { padding: 0 6px; }
}
.guide-said-right { grid-template-columns: minmax(0, 1fr) 132px; }
.guide-said-right .guide-speaker { order: 2; }
.guide-speaker {
  position: sticky; top: 96px; text-align: center;
  opacity: 0; transition: opacity .55s ease, transform .55s cubic-bezier(.22, 1, .36, 1);
}
.guide-said-left  .guide-speaker { transform: translateX(-48px); }
.guide-said-right .guide-speaker { transform: translateX(48px); }
.guide-said.is-said .guide-speaker { opacity: 1; transform: translateX(0); }
.guide-speaker-face {
  width: 124px; height: 124px; display: block; margin: 0 auto;
  filter: drop-shadow(0 10px 20px rgba(60, 44, 18, .35));
}
.guide-speaker-name {
  display: block; margin-top: 8px; color: #7c6d55;
  font-family: var(--serif); font-size: .84rem; line-height: 1.35;
}
@media (max-width: 860px) {
  /* On a phone there is no side to come in from: the speaker becomes a small
     face above what they are saying. */
  .guide-said, .guide-said-right { grid-template-columns: 1fr; gap: 10px; }
  .guide-said-right .guide-speaker { order: 0; }
  .guide-speaker { position: static; display: flex; align-items: center; gap: 12px; text-align: left; }
  .guide-said-left .guide-speaker, .guide-said-right .guide-speaker { transform: translateY(16px); }
  .guide-said.is-said .guide-speaker { transform: translateY(0); }
  .guide-speaker-face { width: 62px; height: 62px; margin: 0; }
  .guide-speaker-name { margin-top: 0; }
}
@media (prefers-reduced-motion: reduce) {
  .guide-speaker { opacity: 1 !important; transform: none !important; transition: none; }
}

/* ── the four things you do ──────────────────────────────────────────────
   Aug 12 2026 — "instead of 5 screens put 4 things that are on the side...
   for each thing a wooden frame and inside for some specific text - scrolls.
   Use more icons there."

   The picker is a column of carved plaques down the LEFT. The capture hangs
   in a wooden board, and three small scrolls of hard figures sit INSIDE that
   board along its foot — each with the game's own icon, so a number is read
   rather than looked up. The paragraph goes on a scroll beneath the board. */
.guide-four {
  display: grid; grid-template-columns: 254px minmax(0, 1fr);
  gap: 26px; align-items: start; outline: none; margin-top: 6px;
}
.guide-four-nav { display: flex; flex-direction: column; gap: 10px; position: sticky; top: 96px; }
.guide-four-tab {
  display: flex; align-items: center; gap: 12px; cursor: pointer; margin: 0;
  padding: 0 14px 0 10px; min-height: 62px; width: 100%; text-align: left;
  opacity: .6; filter: saturate(.65);
  transition: opacity .2s, filter .2s, transform .2s;
}
.guide-four-tab:hover { opacity: .9; filter: none; }
.guide-four-tab.on { opacity: 1; filter: none; transform: translateX(5px); }
.guide-four-tab-ico {
  width: 34px; height: 34px; object-fit: contain; flex: none;
  filter: drop-shadow(0 2px 4px rgba(0, 0, 0, .7));
}
.guide-four-tab-name {
  font-family: var(--serif); font-size: 1rem; line-height: 1.2; color: #f6ecd2;
  text-shadow: 0 2px 5px rgba(0, 0, 0, .85);
  white-space: nowrap;
}

.guide-four-board {
  position: relative; display: block; padding: 0;
  border-width: 31px 52px 32px 53px;
  border-image-width: 31px 52px 32px 53px;
}
.guide-four-shot { width: 100%; display: block; }
/* The scrolls sit INSIDE the board, on the planks below the picture rather
   than over it — three chips laid across a capture cover the thing they are
   describing, which was the first thing the layout got wrong. The wood shows
   between and around them, so they are plainly part of the same object. */
.guide-four-facts {
  display: flex; gap: 10px; flex-wrap: wrap; justify-content: center;
  padding: 14px 4px 4px;
}
.guide-fact {
  padding: 8px 12px;
  border: solid transparent;
  border-width: 22px 26px 23px 26px;
  border-image: url('/assets/ui/scroll_frame.png?v=49') 175 160 175 160 fill / 22px 26px 23px 26px stretch;
  filter: drop-shadow(0 5px 12px rgba(16, 11, 4, .55));
  flex: 1 1 176px; min-width: 0; max-width: 100%;
}
.guide-fact-ico { width: 26px; height: 26px; object-fit: contain; flex: none; }
/* icon on the left, the label above its figure on the right */
.guide-fact-ico { align-self: center; }
.guide-fact i {
  font-style: normal; font-size: .6rem; letter-spacing: .1em; text-transform: uppercase;
  color: #8a7757; display: block; margin-bottom: 1px;
}
.guide-fact b { display: block; font-size: .84rem; color: #2a2013; line-height: 1.25; }
.guide-fact > i, .guide-fact > b { flex: 0 1 auto; }
.guide-fact { display: grid; grid-template-columns: auto 1fr; grid-template-areas: 'ico label' 'ico value'; column-gap: 9px; }
.guide-fact .guide-fact-ico { grid-area: ico; }
.guide-fact i { grid-area: label; }
.guide-fact b { grid-area: value; }
/* Aug 12 2026 — this ink was chosen against the white chapter sheet. With the
   sheet gone the caption sits on the dark field art, where #7c6d55 is nearly
   invisible; it reads as parchment now, with a shadow to lift it off the
   photograph the way every other caption on the dark pages does. */
.guide-four-cap {
  margin: 12px auto 0; max-width: 70ch; text-align: center;
  color: #ddd0b4; font-size: .88rem; font-style: italic;
  text-shadow: 0 2px 8px rgba(0, 0, 0, .85);
}
.guide-four-say {
  margin-top: 16px;
  border: solid transparent;
  border-width: 74px 62px 76px 62px;
  border-image: url('/assets/ui/scroll_frame.png?v=49') 175 160 175 160 fill / 74px 62px 76px 62px stretch;
  background: none; padding: 0;
  filter: drop-shadow(0 12px 28px rgba(60, 44, 18, .3));
}
.guide-four-say-inner { padding: 0 22px; }
.guide-four-say h2 { font-family: var(--serif); font-size: 1.4rem; color: #241a0e; margin: 0 0 8px; }
.guide-four-say p { color: #4a3a24; line-height: 1.7; margin: 0; }
@keyframes guide-swap { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: none; } }
.guide-four-view.is-swap .guide-four-board,
.guide-four-view.is-swap .guide-four-say { animation: guide-swap .34s ease both; }
.guide-four-view.is-swap .guide-four-say { animation-delay: .05s; }
@media (max-width: 900px) {
  .guide-four { grid-template-columns: 1fr; gap: 14px; }
  .guide-four-nav { position: static; flex-direction: row; flex-wrap: wrap; }
  .guide-four-tab { width: auto; min-height: 46px; }
  .guide-four-tab-name { font-size: .88rem; }
  .guide-four-board { border-width: 18px 30px 19px 31px; border-image-width: 18px 30px 19px 31px; }
  /* Under the picture rather than over it: three scrolls on a phone-width
     capture would cover the thing they are describing. */
  .guide-four-facts { position: static; margin-top: 10px; }
  .guide-four-say { border-width: 56px 40px 58px 40px; border-image-width: 56px 40px 58px 40px; }
  .guide-four-say-inner { padding: 0 6px; }
}

/* ── medallions: creatures and troops, no stats ──────────────────────────
   "Don't put creatures and ranks in the wooden frames, better display them in
   small circle frames, without any stats." The game's own bronze medallion
   surround, the portrait inside it, the name underneath, and nothing else. */
.guide-medallions {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(104px, 1fr));
  gap: 16px 12px; margin: 16px 0 6px;
}
.guide-med { margin: 0; text-align: center; }
/* A real circle. The medallion_frame art is a rounded SQUARE plate, which read
   as a tile rather than a medallion when forty of them sit in a grid — so the
   ring is drawn instead, in the same bronze-on-parchment the battle screen's
   unit chips already use. */
.guide-med-ring {
  display: flex; align-items: center; justify-content: center;
  width: 96px; height: 96px; margin: 0 auto;
  border-radius: 50%; overflow: hidden;
  background:
    radial-gradient(circle at 50% 38%, #fdf7e6 0%, #f2e6c9 62%, #e6d5ac 100%);
  border: 3px solid #b08f4f;
  box-shadow:
    0 0 0 2px rgba(120, 92, 48, .35),
    inset 0 2px 6px rgba(255, 255, 255, .6),
    inset 0 -6px 12px rgba(120, 92, 48, .18);
  filter: drop-shadow(0 6px 14px rgba(60, 44, 18, .3));
  transition: transform .2s, border-color .2s;
}
.guide-med:hover .guide-med-ring { border-color: var(--gold); }
.guide-med:hover .guide-med-ring { transform: translateY(-3px) scale(1.04); }
.guide-med-ring img { width: 74%; height: 74%; object-fit: contain; }
.guide-med figcaption {
  margin-top: 7px; font-family: var(--serif); font-size: .84rem;
  color: #3d2a10; line-height: 1.25;
}
.guide-medallions-legends { grid-template-columns: repeat(auto-fill, minmax(124px, 1fr)); }
.guide-medallions-legends .guide-med-ring { width: 116px; height: 116px; }
@media (max-width: 620px) {
  .guide-medallions { grid-template-columns: repeat(auto-fill, minmax(86px, 1fr)); gap: 12px 8px; }
  .guide-med-ring { width: 78px; height: 78px; }
  .guide-medallions-legends .guide-med-ring { width: 92px; height: 92px; }
}

/* ── the ranks, on one scroll ────────────────────────────────────────────
   "Ranks you can display on one scroll and show how much glory is which."
   Ten lines on a single sheet: crest, name, leader dots, the Glory. */
.guide-ranks {
  margin: 18px 0 6px;
  border: solid transparent;
  border-width: 84px 66px 86px 66px;
  border-image: url('/assets/ui/scroll_frame.png?v=49') 175 160 175 160 fill / 84px 66px 86px 66px stretch;
  background: none; padding: 0;
  filter: drop-shadow(0 14px 32px rgba(60, 44, 18, .32));
}
.guide-ranks-inner { padding: 0 22px; }
.guide-ranks-head {
  display: flex; justify-content: space-between; gap: 12px;
  padding-bottom: 8px; margin-bottom: 6px;
  border-bottom: 1px solid rgba(122, 90, 58, .35);
}
.guide-ranks-head span {
  font-size: .66rem; letter-spacing: .12em; text-transform: uppercase; color: #8a7757;
}
.guide-rank {
  display: flex; align-items: center; gap: 12px; padding: 7px 0;
  border-bottom: 1px solid rgba(122, 90, 58, .16);
}
.guide-rank:last-child { border-bottom: none; }
.guide-rank-crest { flex: none; width: 40px; height: 40px; display: block; }
.guide-rank-crest img { width: 100%; height: 100%; object-fit: contain; }
.guide-rank-name { font-family: var(--serif); font-size: 1rem; color: #241a0e; flex: none; }
.guide-rank-dots {
  flex: 1 1 auto; min-width: 12px; height: 1px;
  border-bottom: 1px dotted rgba(122, 90, 58, .55); transform: translateY(4px);
}
.guide-rank-glory {
  font-family: var(--serif); font-size: 1rem; color: #7a520f; flex: none; white-space: nowrap;
}
@media (max-width: 720px) {
  .guide-ranks { border-width: 62px 42px 64px 42px; border-image-width: 62px 42px 64px 42px; }
  .guide-ranks-inner { padding: 0 6px; }
  .guide-rank-crest { width: 32px; height: 32px; }
  .guide-rank-name, .guide-rank-glory { font-size: .9rem; }
}

/* ── Glory, in the game's own crests ─────────────────────────────────────
   "use ... icons from the game instead of emojis (for example to explain
   glory ranks)". Ten painted medallions, in a ladder, where three medal
   emojis used to be repeated four, three and three times over. */
.guide-leagues {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(126px, 1fr));
  gap: 12px; margin: 18px 0 6px;
}
.guide-league {
  margin: 0; padding: 12px 8px; text-align: center;
  border: 1px solid #e0d0a8; border-radius: 12px; background: #fdf8ea;
  display: flex; flex-direction: column; align-items: center; gap: 8px;
}
.guide-league-crest { display: block; width: 74px; height: 74px; }
.guide-league-crest img { width: 100%; height: 100%; object-fit: contain; filter: drop-shadow(0 5px 10px rgba(60, 44, 18, .28)); }
.guide-league figcaption { display: flex; flex-direction: column; gap: 2px; }
.guide-league figcaption b { color: #241a0e; font-family: var(--serif); font-size: .96rem; }
.guide-league-min { color: #7c6d55; font-size: .78rem; }

.guide-toc { display: flex; flex-wrap: wrap; gap: 8px; margin: 0 0 24px; }
.guide-chapter-bare .guide-toc a {
  background: rgba(23, 17, 6, .8); border-color: rgba(212, 160, 23, .35); color: #e6d8b6;
  backdrop-filter: blur(2px);
}
.guide-chapter-bare .guide-toc a:hover { background: rgba(212, 160, 23, .28); color: #fff6d6; }
.guide-toc a {
  padding: 7px 14px; border: 1px solid #e0d0a8; border-radius: 999px;
  background: #fdf8ea; color: #6b4f22; text-decoration: none;
  font-size: .9rem; cursor: pointer; transition: background .18s, border-color .18s;
}
.guide-toc a:hover { background: #f6e9c6; border-color: var(--gold); color: #3d2a10; }
.guide-mech { scroll-margin-top: 84px; }
.guide-mech h2 { font-family: var(--serif); color: #241a0e; }
.guide-mech p { color: #4a3d2c; line-height: 1.75; margin: 0 0 14px; }
.guide-mech p b { color: #241a0e; }
/* The figures, pulled out of the prose so a player who only wants the numbers
   can find them without reading four paragraphs to get there. */
.guide-facts {
  list-style: none; margin: 16px 0 0; padding: 14px 18px;
  border-left: 4px solid var(--gold); border-radius: 0 8px 8px 0;
  background: #fdf8ea;
}
.guide-facts li { color: #5a4a33; line-height: 1.6; margin: 0 0 7px; font-size: .95rem; }
.guide-facts li:last-child { margin-bottom: 0; }
.guide-facts li::before { content: '·'; color: var(--gold); font-weight: 700; margin-right: 9px; }
.guide-facts b { color: #241a0e; }

@media (max-width: 860px) {
  .guide-step { grid-template-columns: 1fr; gap: 20px; padding: 28px 0; }
  .guide-step:nth-child(even) .guide-step-text { order: 0; }
  .guide-duo { grid-template-columns: 1fr; }
}

/* ── Admin console: its own door and its own masthead (Aug 6 2026) ────────── */
.admin-console { min-height: 100vh; background: var(--surface); }
.admin-masthead { background: #171106; border-bottom: 2px solid var(--gold); }
.admin-masthead-inner {
  width: min(1400px, 96vw); margin: 0 auto; padding: 12px 0;
  display: flex; align-items: center; gap: 20px; flex-wrap: wrap;
}
.admin-brand { display: flex; align-items: center; gap: 10px; flex: 1; }
.admin-brand img { width: 38px; height: 38px; object-fit: cover; border-radius: 50%; }
.admin-brand span {
  font-family: var(--serif); font-size: 1.1rem; font-weight: 700; color: #f6ecd8;
  letter-spacing: .08em;
}
.admin-whoami { color: #d9cbb0; font-size: .9rem; }
.admin-whoami .muted { color: #8d7f68; }
.admin-masthead-actions { display: flex; gap: 8px; }
.admin-console .admin-page {
  width: min(1400px, 96vw); margin: 0 auto; padding: 18px 0 60px;
}

.admin-gate {
  min-height: 100vh; display: flex; align-items: center; justify-content: center;
  padding: 40px 20px; background: #171106;
}
.admin-gate-card {
  width: min(400px, 92vw); display: flex; flex-direction: column; gap: 14px;
  padding: 34px 32px 26px; border-radius: 14px;
  background: #211809; border: 1px solid rgba(212, 160, 23, .35);
  box-shadow: 0 24px 60px rgba(0, 0, 0, .55);
}
.admin-gate-mark { width: 56px; height: 56px; object-fit: cover; border-radius: 50%; align-self: center; }
.admin-gate-card h1 {
  font-family: var(--serif); font-size: 1.5rem; color: #f6ecd8; margin: 0; text-align: center;
  letter-spacing: .06em;
}
.admin-gate-sub { margin: -8px 0 6px; text-align: center; color: #a2937a; font-size: .88rem; }
.admin-gate-card .admin-field-label { color: #a2937a; }
.admin-gate-card input {
  padding: 10px 12px; border-radius: 8px; width: 100%; font: inherit;
  background: #14100a; border: 1px solid rgba(212, 160, 23, .3); color: #f4ecdc;
}
.admin-gate-card input:focus { outline: none; border-color: var(--gold); box-shadow: 0 0 0 3px rgba(212, 160, 23, .18); }
.admin-gate-err { color: #f0a094; font-size: .86rem; min-height: 18px; text-align: center; }
.admin-gate-back { text-align: center; color: #8d7f68; text-decoration: none; font-size: .84rem; }
.admin-gate-back:hover { color: var(--gold-bright); }

@media (max-width: 700px) {
  .admin-masthead-inner { gap: 10px; }
  .admin-whoami { order: 3; width: 100%; }
}

/* Registration long-form: consent rows */
.landing-check { display: flex; gap: 9px; align-items: flex-start; font-size: .84rem; color: var(--on-surface-var); }
.landing-check[hidden] { display: none; }
.landing-check input { margin-top: 3px; flex-shrink: 0; }
.landing-check a { color: var(--secondary); }

/* Type-ahead field in admin forms */
.admin-datalist { display: block; }
.admin-datalist input { width: 100%; }

/* Registration: name+surname side by side, and the city-state select */
.landing-duo.landing-reg { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
/* inputs otherwise keep their intrinsic ~20ch width and overflow the card */
.landing-duo.landing-reg .landing-field { min-width: 0; }
.landing-duo.landing-reg input { width: 100%; min-width: 0; }
.landing-duo.landing-reg[hidden] { display: none; }
.landing-field select {
  padding: 10px 12px; border-radius: 8px; border: 1px solid var(--outline-var);
  background: rgba(255, 253, 245, .92); font: inherit; color: var(--on-surface); width: 100%;
}

/* Site footer identity block — year/domain, then the company's own details */
.site-footer-id { display: flex; flex-direction: column; gap: 2px; }
.site-footer-line { font-size: .88rem; }
.site-footer-id .site-footer-line:first-child { font-weight: 700; }

/* Invoice line editor */
.admin-lines { display: flex; flex-direction: column; gap: 8px; }
.admin-lines-rows { display: flex; flex-direction: column; gap: 6px; }
.admin-line { display: grid; grid-template-columns: 1fr 90px auto; gap: 8px; align-items: center; }
.admin-line select, .admin-line input { width: 100%; min-width: 0; }
.admin-lines-actions { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
.admin-lines-total { font-size: .84rem; }

/* ----- shop: dual pricing + price filter (Aug 6 2026) ----- */
/* Every card now ends in two buy buttons — money and Ambrosia — so the pair
   needs to sit as one unit at the bottom of the card whatever height the card
   above it happens to be. */
.buy-row {
  display: flex; gap: 8px; justify-content: center; flex-wrap: wrap;
  margin-top: auto; padding-top: 4px;
}
.buy-row .btn { flex: 1 1 auto; min-width: 92px; white-space: nowrap; }
.card { display: flex; flex-direction: column; }

/* The housing line on a creature bundle, when the squad will not fit. Not an
   error — the card is still worth reading — so it warns rather than shouts. */
.shop-tight { color: #c9772e; }

/* Aug 8 2026 — the two modes are carved plates now and carry their own frame,
   so the dark strip they used to sit on has gone: a box around a box. */
.shop-filter {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  margin: 0 0 14px; padding: 0;
  background: none; border: none; border-radius: 0;
}
.shop-filter .label-caps { margin-right: 4px; }
/* One class hides both a filtered-out card and a section left with none. */
.is-filtered { display: none !important; }
/* "Showing the newest 300 of 412" over a capped admin table. */
.admin-cap-note { margin: 0 0 10px; font-size: 13px; }

/* ----- army: three zones, split capacity, drag to move (Aug 6 2026) ----- */
/* Two meters, not three, since the reserve bar came off (Aug 8 2026) — and
   the hourly bill spans the pair rather than squatting in a third column. */
.housing-meters { display: grid; grid-template-columns: repeat(2, 1fr); gap: 14px; width: 100%; }
.housing-meters > .upkeep-row { grid-column: 1 / -1; justify-content: center; }
@media (max-width: 900px) { .housing-meters { grid-template-columns: 1fr; } }
.hm-fill-gar { background: linear-gradient(90deg, #6f8f5a, #a8c47e); }
/* The reserve has no cap to fill, so its bar is a presence indicator rather
   than a proportion — muted on purpose so it does not read as "full". */
.hm-fill-res { background: linear-gradient(90deg, #3f5f78, #6f97b5); opacity: .75; }
.reserve-meter .housing-meter-track { opacity: .8; }

.army-zones { display: grid; grid-template-columns: repeat(3, 1fr); gap: 4px; align-items: start; }
@media (max-width: 1100px) { .army-zones { grid-template-columns: 1fr; } }
.army-zone { transition: box-shadow .15s ease, border-color .15s ease, transform .15s ease; }

/* ----- Three scrolls, one per army zone (Aug 8 2026) -----
   "use 3 different design of the scrolls for raid army background (red),
   defence (blue), reserve (yellow)". The same scroll three ways, told apart
   by the colour of its gemstone finials — so the set reads as a set, and
   where a stack is headed is legible mid-drag rather than only from the
   heading. The art is 878×889 and the slices are measured off it: the
   parchment begins 172px down, its torn left edge wanders in to 180px, the
   right to 178px, and the lower meander band starts 159px up. */
.zone-scroll {
  border: solid transparent;
  border-width: 64px 62px 62px 63px;
  border-image-slice: 172 178 159 180 fill;
  border-image-width: 64px 62px 62px 63px;
  border-image-repeat: stretch;
  background: none; border-radius: 0; box-shadow: none; padding: 0;
  min-height: 300px;
}
.zone-raid { border-image-source: url('/assets/ui/scroll_raid.png?v=32'); }
.zone-def  { border-image-source: url('/assets/ui/scroll_defence.png?v=32'); }
.zone-res  { border-image-source: url('/assets/ui/scroll_reserve.png?v=32'); }
/* The contents sit on parchment now, so they are inked rather than lit. */
.zone-inner { color: #2a2013; --backdrop: 232, 220, 194; padding: 0 12px; }
.zone-inner h3 { color: #2a2013; margin-top: 0; }
.zone-inner .muted { color: #6d5a3e; }
.zone-inner .zone-hint { color: #7a520f; opacity: 1; }
.zone-inner .roster-medallions { justify-content: center; gap: 14px 18px; }
/* The stack counts were cream, for the dark panels these zones used to be — on
   parchment that is nearly invisible. Inked, with the legend tag darkened to
   match. */
.zone-inner .roster-medallion b { color: #43301a; }
.zone-inner .rm-legend-tag { color: #8a5f14; }
.zone-inner .label-caps { color: #7a520f; }
/* Drop feedback lives inside the scroll: a border-image cannot change colour
   on hover without swapping the whole artwork. */
.army-zone.is-target .zone-inner { outline: 2px dashed rgba(122, 90, 58, .55); outline-offset: 10px; border-radius: 8px; }
.army-zone.is-over { transform: translateY(-2px); }
.army-zone.is-over .zone-inner { outline-color: #a8762a; outline-style: solid; }
@media (max-width: 1100px) {
  .zone-scroll { border-width: 84px 60px 82px 61px; border-image-width: 84px 60px 82px 61px; }
}

.zone-hint {
  font-family: var(--sans, inherit); font-size: 11px; letter-spacing: .14em;
  text-transform: uppercase; color: var(--gold-bright); opacity: .75; margin-left: 6px;
}
/* Aug 7 2026 — "Make Across The Sea white and RESERVE yellow", then "Change
   places of Across the Sea and RESERVE": the place is the white headline, the
   gold caps beside it say what that place is. The .dark panel already paints
   h3 white and .zone-hint gold, so the two just swapped words. */

/* The two panels closing the Army page are one row: same dark treatment as
   the zones above, and the same height whichever has more in it. */
/* Shop section headings wear the user's category renders instead of an emoji. */
.sec-h { display: flex; align-items: center; gap: 12px; }
.sec-ico { width: 54px; height: 54px; object-fit: contain; flex: 0 0 auto; }
@media (max-width: 640px) { .sec-ico { width: 40px; height: 40px; } }

/* ----- The muster tent (Aug 8 2026) -----
   "In new folder you can find new tent frame for top bar with all the troop
   housing bar."  tent_bar.png is 1299×636: the canopy is the top 262px, the
   torn parchment edges reach 125px in on the left and 120px on the right, and
   the lower meander band starts 80px up.

   The canopy is deliberately drawn at less than its natural height — at full
   scale a 1200px-wide bar would carry a 212px roof and the page would open
   with a picture rather than a status line. 128px keeps the tent legible and
   the bar a bar. */
.page-banner.tent-bar {
  border-style: solid; border-color: transparent;
  /* Aug 8 2026 — "top part of the frame missing some part, it looks bad".
     A border-image's four CORNERS are drawn at the border widths you give it,
     never stretched — so a 125×262 corner squeezed into 31×128 was crushing
     the canopy's overhang to less than half its width and chopping its ends
     flat. The widths below hold the source ratio (×0.489 on every side), so
     each corner is scaled rather than squashed and the tent keeps its shape.
     Padding then keeps the text off the meander band and the torn edges. */
  border-width: 128px 59px 39px 61px;
  border-image: url('/assets/ui/tent_bar.png?v=32') 262 120 80 125 fill / 128px 59px 39px 61px stretch;
  background: none; border-radius: 0; box-shadow: none;
  padding: 14px 18px 10px;
  max-width: 1180px; margin-inline: auto;
}
.page-banner.tent-bar { --backdrop: 236, 224, 198; }
.tent-bar .meta { color: #6b4d12; margin-top: -4px; }
.tent-bar .housing-meter { margin-top: 8px; }
.tent-bar .housing-meter-head { color: #3a2d18; }
.tent-bar .label-caps { color: #6b4d12; }
.tent-bar .muted { color: #6a5636; }
.tent-bar .hm-train { color: #7a520f; }
.tent-bar .hm-occ, .tent-bar .hm-cap { color: #4a3a24; }
.tent-bar .upkeep-row { border-top-color: rgba(122, 90, 58, .35); }
@media (max-width: 900px) {
  .page-banner.tent-bar { border-width: 84px 39px 26px 40px; border-image-width: 84px 39px 26px 40px; padding: 10px 10px 6px; }
}

/* Train troops lost its cream slab — the unit cards below carry their own. */
.train-sec { background: none; border: none; box-shadow: none; padding: 0; }

/* Renders standing in for emoji (Aug 8 2026 — trophy, cart, armillary sphere).
   Baseline-shifted so each sits in a line of text the way a glyph would. */
.glyph { object-fit: contain; vertical-align: -.22em; flex: 0 0 auto; }
.buy-btn .glyph { vertical-align: -.28em; }
.cart-fab .glyph { vertical-align: middle; }
.k-lead-title .glyph, .cart-drawer-head .glyph { vertical-align: -.16em; }

/* The weekly-claim button stands alone now that the Koinon banner section is
   gone; centred so it does not read as an orphan. */
.shop-claim { display: flex; justify-content: center; margin-top: 6px; }
.shop-claim:empty { display: none; }

/* ----- The shop, unboxed (Aug 8 2026) -----
   "For all the categories remove white background and use wooden frame in the
   new folder for their titles. Also you big wooden frame for items and you can
   put a light glowing highlight around frames."

   A section is no longer a cream card holding other cards — a box inside a
   box. The section itself is now nothing at all; its title rides the carved
   plank, and every product wears the laurel-columned wooden frame. */
.shop-sec { background: none; border: none; box-shadow: none; padding: 0; border-radius: 0; }
.shop-sec > .muted {
  color: #e6dcc4; text-shadow: 0 1px 3px rgba(0, 0, 0, .75);
  max-width: 940px; margin-inline: auto; text-align: center;
}

/* The title plank. Sliced off plank_title.png (1149×353): the meander band is
   the top 78px and the bottom 68px, the plank's chewed ends the outer 30px.
   The bands REPEAT rather than stretch — a meander pulled to double width
   stops being a meander. */
/* Aug 12 2026 — the plank is also used ON A <button> (the four-things nav), and
   a button with no `background` declared paints the UA's ButtonFace (#efefef)
   across its whole border box. The plank art has transparent corners, so that
   grey showed through as a white rectangle behind every plaque. It had been
   invisible only because the chapter's white sheet sat behind it; removing the
   sheet exposed it. `appearance: none` as well, because some UAs paint a native
   button background that `background: none` alone does not cancel. */
.sec-plank {
  -webkit-appearance: none; appearance: none;
  background: none;
  border-style: solid; border-color: transparent;
  border-width: 21px 26px 18px 26px;
  border-image: url('/assets/ui/plank_title.png?v=32') 78 30 68 30 fill / 21px 26px 18px 26px round stretch;
  min-height: 76px; padding: 0 26px;
  display: flex; align-items: center; justify-content: center; gap: 12px;
  margin: 0 auto 14px; max-width: 860px;
  color: #f6ecd2; text-shadow: 0 2px 4px rgba(0, 0, 0, .8);
  text-align: center;
}
.sec-plank .sec-ico { filter: drop-shadow(0 2px 3px rgba(0, 0, 0, .6)); }
@media (max-width: 640px) {
  .sec-plank { border-width: 16px 20px 14px 20px; border-image-width: 16px 20px 14px 20px; min-height: 60px; }
}

/* The product frame. frame_wood.png is 978×959 — laurel pilasters 118px and
   116px wide, meander caps 68px and 70px tall. Contents now sit on dark wood,
   so everything inside is relit: cream headings, warm tan for the small print.
   The glow is a box-shadow, NOT a filter — a filter on a border-image forces a
   re-rasterisation on every hover, which is exactly what made the scroll
   flicker under the cursor a round ago. */
.cards > .card {
  border-style: solid; border-color: transparent;
  border-width: 22px 19px 22px 19px;
  border-image: url('/assets/ui/frame_wood.png?v=32') 68 116 70 118 fill / 22px 19px 22px 19px stretch;
  /* 23px, not 27: the widest price in the shop ("€1000.00 · 120k") has to fit
     between the plate's own caps on the narrowest card, and every pixel the
     pilaster gives up is a pixel the price gets. */
  background: none; border-radius: 0; padding: 6px 2px 10px;
  box-shadow: 0 0 18px rgba(212, 160, 23, .20);
  transition: box-shadow .18s ease, transform .12s ease;
  color: #f2e8d0;
}
.cards > .card:hover { box-shadow: 0 0 26px rgba(232, 190, 70, .42); transform: translateY(-2px); }
.cards > .card h3, .cards > .card b { color: #f7edd4; text-shadow: 0 1px 3px rgba(0, 0, 0, .7); }
.cards > .card { --backdrop: 58, 38, 22; }
.cards > .card .muted, .cards > .card .label-caps { color: #cbb68e; }
/* The resource chips kept their pale pill from the cream-card days — cream
   text on a cream pill, which the contrast audit caught at 1:1. */
.cards > .card .bundle-line, .cards > .card .qs-item {
  background: rgba(24, 15, 6, .42); color: #f0e4c8; border: 1px solid rgba(212, 160, 23, .28);
}
.cards > .card .shop-tight { color: #f0a860; }
/* The disc keeps a pale mount so the render still reads against the wood. */
.cards > .card img.big { border-color: #8a6a3e; box-shadow: 0 2px 8px rgba(0, 0, 0, .5); }
/* "Active" cards get a stronger halo instead of a border colour they can no
   longer show through the artwork. */
.cards > .card.gold-frame { box-shadow: 0 0 30px rgba(232, 190, 70, .58); }
.cards > .card.legend { box-shadow: 0 0 26px rgba(212, 160, 23, .40); }

/* Stack stat card (double-click a medallion). Aug 8 2026 — rebuilt lighter:
   the game's own stat icons carry the meaning, so the boxes and rules came
   off and only numbers remain. */
.stack-stats .ss-head { display: flex; align-items: center; gap: 14px; margin-bottom: 18px; padding-right: 56px; }
/* …the ✕ lives at the top-right of the scroll, so the power figure keeps clear of it. */
.stack-stats .ss-head h2 { margin: 0; }
.ss-power { margin-left: auto; text-align: right; }
.ss-power b { font-size: 24px; color: #6b4d12; display: block; line-height: 1.1; }
.ss-label { display: block; font-size: 10px; letter-spacing: .12em; text-transform: uppercase; color: var(--on-surface-var); }
.ss-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 14px 8px; margin-bottom: 16px; }
.ss-stat { display: flex; flex-direction: column; align-items: center; gap: 2px; text-align: center; }
.ss-stat b { font-size: 17px; line-height: 1.1; }
.ss-ico { width: 26px; height: 26px; object-fit: contain; margin-bottom: 2px; }
.ss-traits { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 14px; justify-content: center; }
.ss-trait { font-size: 12px; padding: 2px 9px; border-radius: 999px; background: rgba(122, 90, 58, .12); }
.ss-cost { display: flex; gap: 22px; justify-content: center; flex-wrap: wrap;
  border-top: 1px dashed rgba(122, 90, 58, .3); padding-top: 12px; }
.ss-costline { display: flex; align-items: center; gap: 8px; }
.ss-costline .ss-label { display: inline-block; }
.ss-res { display: inline-flex; align-items: center; gap: 4px; font-weight: 700; }
.ss-res img { width: 20px; height: 20px; object-fit: contain; }
@media (max-width: 560px) { .ss-grid { grid-template-columns: repeat(2, 1fr); } }

.army-bottom { height: 100%; }
/* A legend sitting in the reserve: same medallion, but it wears a name plate
   and does not drag (it has no housing to ferry and is always battle-ready). */
.roster-medallion.is-legend { cursor: default; }
.roster-medallion.is-legend .icon-frame.med-sm { box-shadow: 0 0 0 2px var(--gold-bright); }
.rm-legend-tag { display: block; font-size: 10px; color: var(--gold-bright); margin-top: 2px; text-align: center; }
.grid2:has(.army-bottom) { align-items: stretch; }
.grid2:has(.army-bottom) > div { display: flex; }
.fav-locked { opacity: .55; }
.fav-locked .label-caps { color: var(--gold-bright); white-space: nowrap; }
/* Aug 11 2026 — the favours moved onto the parchment scroll, so the one colour
   that was tuned for a dark slab has to be retuned for it: gold-bright on
   parchment measures under 2:1. The scroll is wider than the Koinon's side
   panel because a favour row carries a medallion, a name, a sentence and a
   button. */
/* .k-scroll.k-scroll-sm carries max-width:560 at two class tokens, so this has
   to match it token for token or the scroll stays the Koinon's side-panel
   width — which for a row of medallion + name + sentence + button is four
   words to a line. */
.k-scroll.army-favors { max-width: 880px; margin: 22px auto 0; }
.army-favors .k-scroll-inner { padding: 0 40px; }
.army-favors .fav-locked .label-caps { color: #7a520f; }
/* The "needs Temple of X" note was nowrap — written for a wide dark panel — and
   on the scroll it held open a column two hundred pixels wide, squeezing every
   favour's description into four words a line. It wraps now, right-aligned, and
   the medallion is pinned so the rows line up down the parchment. */
.army-favors .fav-locked .label-caps {
  white-space: normal; text-align: right; max-width: 132px; line-height: 1.35;
}
.army-favors .row { gap: 14px; align-items: center; }
.army-favors .fav-medal { width: 52px; height: 52px; object-fit: contain; flex: 0 0 52px; }
.army-favors .row.sel { background: rgba(122, 82, 15, .13); }
.army-favors .row .grow { min-width: 0; }
@media (max-width: 720px) {
  .army-favors .k-scroll-inner { padding: 0 18px; }
  .army-favors .fav-locked .label-caps { max-width: 96px; font-size: 10px; }
}

/* The standing army's hourly bill, under the housing meters. */
.upkeep-row {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  margin-top: 10px; padding-top: 10px; border-top: 1px dashed rgba(212,160,23,.3);
}
.upkeep-lines { display: inline-flex; gap: 12px; flex-wrap: wrap; }
.upkeep-line { display: inline-flex; align-items: center; gap: 5px; font-weight: 700; font-size: 14px; }
.upkeep-line img { width: 20px; height: 20px; object-fit: contain; }
.upkeep-warn { color: #e0705a; font-size: 13px; }
.upkeep-row.upkeep-short { border-top-color: rgba(224,112,90,.5); }
/* A zone lights up while a stack is over it, and more softly while one is
   merely picked up — the first says "let go here", the second says "you could". */
.army-zone.is-target { border-color: rgba(212, 160, 23, .55); box-shadow: 0 0 0 1px rgba(212, 160, 23, .25); }
.army-zone.is-over {
  border-color: var(--gold-bright);
  box-shadow: 0 0 0 2px rgba(212, 160, 23, .5), 0 8px 24px rgba(0, 0, 0, .35);
  transform: translateY(-1px);
}
.roster-medallion.movable { cursor: grab; }
.roster-medallion.movable:active { cursor: grabbing; }
.roster-medallion.is-dragging { opacity: .4; }
.roster-medallion.is-picked {
  outline: 2px solid var(--gold-bright); outline-offset: 3px; border-radius: 8px;
}
/* Donated troops belong to an ally and cannot be moved — say so quietly. */
.roster-medallion:not(.movable) { cursor: default; opacity: .85; }

/* ----- shopping cart (Aug 6 2026) ----- */
.btn-cart { flex: 0 0 auto !important; min-width: 0 !important; padding-inline: 10px; }
.cart-fab {
  position: fixed; right: 26px; bottom: 96px; z-index: 250;
  width: 56px; height: 56px; border-radius: 50%; font-size: 22px;
  border: 2px solid rgba(212, 160, 23, .55); cursor: pointer;
  background: radial-gradient(circle at 30% 30%, #3a2d1b, #241a0e);
  color: #f2ecdf; box-shadow: 0 6px 18px rgba(0, 0, 0, .45);
  transition: transform .15s ease, box-shadow .15s ease;
}
.cart-fab:hover { transform: translateY(-2px); }
.cart-fab .cart-fab-n {
  position: absolute; top: -6px; right: -6px; min-width: 22px; height: 22px;
  border-radius: 11px; background: var(--gold-bright, #d4a017); color: #241a0e;
  font-size: 13px; font-weight: 700; line-height: 22px; padding: 0 5px;
  display: none;
}
.cart-fab.has-items .cart-fab-n { display: inline-block; }
.cart-box { min-width: min(560px, 92vw); }
.cart-line {
  display: grid; grid-template-columns: 1fr auto auto auto; gap: 10px;
  align-items: center; padding: 8px 0; border-bottom: 1px solid rgba(212, 160, 23, .18);
}
.cart-line-qty { display: flex; align-items: center; gap: 6px; }
.cart-line-price { white-space: nowrap; color: #d9cdb4; font-size: 14px; }
.cart-amb-ico { width: 14px; height: 14px; object-fit: contain; vertical-align: -2px; margin-left: 2px; }
.cart-totals { margin: 14px 0 6px; font-size: 17px; }
.cart-actions { flex-wrap: wrap; }

/* Bundle cards carry a corner price tag so a wall of them scans as a ladder. */
.bundle-card { position: relative; }

/* ----- cart drawer (Aug 6 2026 — slides in from the right) ----- */
.cart-drawer-back {
  position: fixed; inset: 0; z-index: 340; background: rgba(12, 9, 5, .45);
  opacity: 0; transition: opacity .22s ease;
}
.cart-drawer-back.is-in { opacity: 1; }
.cart-drawer {
  position: fixed; top: 0; right: 0; bottom: 0; z-index: 350;
  width: min(440px, 94vw); box-sizing: border-box;
  display: flex; flex-direction: column; gap: 10px;
  padding: 22px 22px 26px; overflow-y: auto;
  background: linear-gradient(180deg, #2a2013, #1d1509);
  border-left: 2px solid rgba(212, 160, 23, .45);
  box-shadow: -14px 0 40px rgba(0, 0, 0, .5);
  color: #f2ecdf;
  transform: translateX(100%); transition: transform .24s ease;
}
.cart-drawer.is-in { transform: none; }
.cart-drawer-head { display: flex; align-items: center; justify-content: space-between; }
.cart-drawer-head h2 { margin: 0; }
.cart-drawer-x { font-size: 18px; line-height: 1; }
.cart-drawer .cart-lines { flex: 0 1 auto; overflow-y: auto; }
.cart-terms {
  display: flex; gap: 10px; align-items: flex-start; font-size: 14px;
  padding: 10px 12px; border: 1px solid rgba(212, 160, 23, .3); border-radius: 8px;
  background: rgba(20, 15, 8, .4); cursor: pointer;
}
.cart-terms input { margin-top: 3px; accent-color: var(--gold-bright, #d4a017); }
.cart-terms a { color: var(--gold-bright, #d4a017); }
.cart-actions { margin-top: 4px; }

/* ----- Carved marble nameplates (Aug 8 2026) -----
   "In folder you can find custom button for shop as well as custom button
   design for all prices in the shop."
   The user drew two plates: bronze meander end-caps ("All Products" /
   "Custom") and chunkier ribbed caps. The lettering was cut out of both and
   the marble between the caps rebuilt as a gradient, so one artwork stretches
   to any label without the original words showing through — see cut_plates.py.
   Only left and right are sliced (top/bottom 0): the plate stretches to the
   button's height, and the caps keep their proportion because their CSS width
   is set from that height. */
/* Specificity has to beat `.btn.primary`, which paints its label near-white —
   invisible on marble. Doubling the class is cheaper than an !important on a
   colour someone may want to theme later. */
.btn.btn-plate, .btn.buy-btn,
.btn.primary.btn-plate, .btn.primary.buy-btn {
  --backdrop: 236, 228, 208;   /* the plate's own marble, for the contrast audit */
  /* Longhands, never the `border` shorthand: the shorthand also resets
     border-image, and from this higher-specificity block that wiped out the
     plate artwork set below. */
  border-style: solid; border-color: transparent;
  background: none !important;
  box-shadow: none !important;
  border-radius: 0;
  color: #43301a; font-weight: 700;
  text-shadow: 0 1px 0 rgba(255, 250, 235, .6);
  transition: filter .12s ease, transform .05s ease;
}
.btn-plate:hover, .buy-btn:hover { box-shadow: none !important; transform: translateY(-1px); }
.btn-plate:active, .buy-btn:active { transform: translateY(1px); box-shadow: none !important; }

/* The small scroll button (Aug 8 2026 — "In folder you can find new button
   that I want to see in the shop"). Same three-slice treatment as the marble
   plates: bronze end-caps held, parchment between them generated, so one
   artwork carries any label at any width. Every remaining plain button in the
   shop now wears it. */
a.btn.btn-scroll { text-decoration: none; }
.btn.btn-scroll, .btn.primary.btn-scroll {
  --backdrop: 207, 178, 145;   /* the scroll's own parchment */
  border-style: solid; border-color: transparent;
  /* Aug 9 2026 — the user's redrawn scroll: meander bands and roller knobs.
     Cropped to content (399x237), caps 117px left / 112px right — at a 40px
     button that is 20px and 19px at the art's true scale. Uneven on purpose;
     evening them out would squash one band more than the other and shift the
     parchment, which is what makes centred text look off-centre. */
  border-width: 0 19px 0 20px;
  border-image: url('/assets/ui/plate_sm.png?v=37') 0 112 0 117 fill / 0 19px 0 20px stretch;
  background: none !important; box-shadow: none !important; border-radius: 0;
  height: 40px; padding: 0 14px; min-width: 108px;
  display: inline-flex; align-items: center; justify-content: center; gap: 6px;
  color: #43301a; font-weight: 700; font-size: 14px;
  text-shadow: 0 1px 0 rgba(255, 250, 235, .6);
  transition: transform .05s ease;
}
.btn.btn-scroll:hover { transform: translateY(-1px); box-shadow: none !important; }
.btn.btn-scroll:active { transform: translateY(1px); box-shadow: none !important; }
.btn.btn-scroll:disabled { filter: grayscale(.45) brightness(.92); }
/* Aug 8 2026 — "change all the scroll buttons that you made greyish to another
   button design." Desaturating the parchment to mark a secondary button made
   it look faded rather than different, so the two states are now two separate
   artworks: the warm parchment scroll leads, the white marble plate follows.
   No filter on either — each is simply itself. */
.btn.btn-scroll:not(.primary) {
  border-image-source: url('/assets/ui/plate_tab.png?v=32');
  border-image-slice: 0 158 0 150 fill;
  border-width: 0 22px 0 21px; border-image-width: 0 22px 0 21px;   /* 150/158 of 289 at 40px */
  --backdrop: 232, 228, 220;
  color: #3f2d18;
}
.btn.btn-scroll.small:not(.primary) { border-width: 0 19px 0 18px; border-image-width: 0 19px 0 18px; }
.btn.btn-scroll.tiny:not(.primary) { border-width: 0 17px 0 16px; border-image-width: 0 17px 0 16px; }
/* A label with an icon beside it centres as a unit, not as text plus a
   floating picture. */
.btn.btn-scroll { text-align: center; }
.btn.btn-scroll.small { height: 34px; min-width: 92px; font-size: 13px; padding: 0 12px;
  border-width: 0 16px 0 23px; border-image-width: 0 16px 0 23px; }
.btn.btn-scroll.tiny {
  height: 32px; min-width: 62px; font-size: 15px; padding: 0 9px;
  border-width: 0 15px 0 22px; border-image-width: 0 15px 0 22px;
}
/* Aug 8 2026 — "Use previously used designs. You can choose them based on the
   color that was previously used just to match."
   The scroll is the one label-agnostic artwork in the set (every btn_*.png
   plaque has its word carved into it and can only ever say that word), so it
   carries every remaining button — and the semantic colour that used to be the
   BUTTON is now the LABEL. Hue-rotating the artwork was the other option and
   it turns the bronze caps green, which is worse than a red word on parchment. */
.btn.btn-scroll.danger, .btn.primary.btn-scroll.danger { color: #8e1b12; }
/* A bare carved icon, no plate behind it: the marble X and the marble arrows
   are already complete buttons in themselves (Aug 8 2026 — "use close icon
   that looks like X that already has been used"). */
.icon-x {
  flex-shrink: 0; width: 30px; height: 30px; padding: 0;
  border: none; background: none; border-radius: 50%;
  display: inline-flex; align-items: center; justify-content: center;
  cursor: pointer; transition: transform .05s, filter .15s;
}
.icon-x img { width: 100%; height: 100%; object-fit: contain; }
.icon-x:hover { filter: brightness(1.12); }
.icon-x:active { transform: translateY(1px) scale(.94); }
.cart-drawer-head .icon-x { width: 34px; height: 34px; }
/* The cart's steppers are the Army's, at cart scale. */
.cart-line-qty .qty-step { width: 28px; height: 28px; }
.btn.btn-scroll.blue, .btn.primary.btn-scroll.blue { color: #17475f; }
.btn.btn-scroll.primary { color: #6b3a06; filter: saturate(1.1) brightness(1.03); }

/* The shop's two mode buttons — the plate the user lettered "All Products". */
.btn.btn-plate {
  height: 46px; padding: 0 18px; min-width: 148px; font-size: 15px; letter-spacing: .03em;
  border-width: 0 25px 0 24px;
  border-image: url('/assets/ui/plate_tab.png?v=32') 0 158 0 150 fill / 0 25px 0 24px stretch;
}
/* Which mode you are in, said with light rather than a different plate. */
.btn.btn-plate.primary { color: #3a2710; filter: saturate(1.12) brightness(1.06); }
.btn.btn-plate:not(.primary) { filter: saturate(.72) brightness(.94); }

/* ----- one-button pricing + the not-enough-Ambrosia pulse (Aug 6 2026) ----- */
/* Every price in the shop is now the second plate. */
/* Padding and gap are trimmed to the bone: the longest price in the shop
   ("€1000.00 · 120k" on a Polis) has to fit between the caps on the narrowest
   card, and at 8px of padding it was three pixels short. */
.btn.buy-btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 4px; white-space: nowrap;
  height: 42px; padding: 0 8px; font-size: 14px;
  /* Aug 9 2026 — the redrawn scroll's caps are near-symmetric (117/112), and
     both are pulled in a shade tighter than true scale (21/20 at 42px) so the
     longest price in the shop ("€1000.00 · 120k") still fits between the
     meander bands on the narrowest card. */
  border-width: 0 20px 0 21px;
  border-image: url('/assets/ui/plate_sm.png?v=37') 0 112 0 117 fill / 0 20px 0 21px stretch;
}
.buy-btn .amb-ico { filter: drop-shadow(0 1px 0 rgba(255, 250, 235, .5)); }
.buy-sep { opacity: .55; margin: 0 1px; color: #6d5a3e; }
@keyframes amb-pulse {
  0%, 100% { box-shadow: 0 0 0 0 rgba(212, 160, 23, 0); }
  30% { box-shadow: 0 0 0 4px rgba(212, 160, 23, .65); }
  60% { box-shadow: 0 0 0 2px rgba(212, 160, 23, .35); }
}
.amb-pulse { animation: amb-pulse 1.05s ease 3; border-radius: 8px; }

/* ----- creature-bundle portrait row (Aug 6 2026) ----- */
/* Every unit type in the bundle, centered — replacing the single (and, with
   tall portrait art, badly cropped) big circle that showed one troop of four. */
.crew-icons {
  display: flex; justify-content: center; align-items: center;
  flex-wrap: wrap; gap: 8px; min-height: 96px;
}
/* Aug 8 2026 — "Remove white in creature bundle." Each portrait sat on a pale
   disc, which was invisible on the old cream card and read as a row of white
   holes once the card became dark wood. The disc is gone; a bronze ring and a
   faint warm wash keep the portrait grouped without a background. */
.crew-portrait {
  position: relative; display: inline-flex; border-radius: 50%;
  border: 3px solid #a8762a; background: rgba(40, 26, 12, .35);
  padding: 3px; box-sizing: border-box;
  box-shadow: 0 2px 6px rgba(0, 0, 0, .45);
}
.crew-portrait img {
  width: 100%; height: 100%; border-radius: 50%;
  /* contain, not cover: the unit portraits are taller than wide, and cover is
     exactly what was cropping heads off in the old single circle. */
  object-fit: contain;
}
.crew-portrait-n {
  position: absolute; right: -4px; bottom: -4px; min-width: 20px; height: 20px;
  border-radius: 10px; padding: 0 5px; line-height: 20px; font-size: 12px;
  text-align: center; background: var(--gold-bright, #d4a017); color: #241a0e;
}

/* ----- custom bundle builder (Aug 6 2026) ----- */
.custom-box { min-width: min(480px, 92vw); }
.custom-rows { display: flex; flex-direction: column; gap: 8px; margin-bottom: 14px; }
.custom-row {
  display: grid; grid-template-columns: 44px 1fr 130px auto; gap: 10px;
  align-items: center; padding: 8px 10px; cursor: text;
  border: 1px solid rgba(212, 160, 23, .25); border-radius: 8px;
  background: rgba(20, 15, 8, .25);
}
.custom-row img, .custom-getrow img, .custom-pick img {
  border-radius: 50%; object-fit: contain; box-sizing: border-box;
  background: var(--surface-cont, #efe7d5); border: 2px solid var(--outline-var, #8a6a48);
  padding: 4px;
}
.custom-row img { width: 40px; height: 40px; }
.custom-row input { width: 100%; text-align: right; }
.custom-row-cap { font-size: 13px; white-space: nowrap; }

/* the Custom market (Aug 7 2026) — wider, with a subtotal column */
.market-box { min-width: min(560px, 94vw); }
.market-box .custom-row { grid-template-columns: 44px 1fr 120px 70px 84px; }
.custom-row-sub { text-align: right; font-size: 13px; white-space: nowrap; }
.custom-row-amb { border-color: rgba(212, 160, 23, .5); background: rgba(212, 160, 23, .08); }

/* Custom: the Budget | Manual switch (Aug 7 2026) */
.custom-tabs { display: flex; gap: 8px; margin-bottom: 14px; }
.custom-budget-row { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 6px; }
.custom-budget-input { font-size: 20px; font-weight: 700; display: inline-flex; align-items: center; gap: 6px; }
.custom-budget-input input { width: 110px; font-size: 18px; text-align: right; }
.custom-focus { display: flex; flex-direction: column; gap: 8px; margin-bottom: 14px; }
.custom-focus-row {
  display: flex; gap: 10px; align-items: flex-start; padding: 9px 12px; cursor: pointer;
  border: 1px solid rgba(212, 160, 23, .25); border-radius: 8px; background: rgba(20, 15, 8, .25);
}
.custom-focus-row input { margin-top: 3px; accent-color: var(--gold-bright, #d4a017); }
.custom-focus-picks { display: flex; flex-wrap: wrap; gap: 8px; padding-left: 8px; }
.custom-pick {
  display: inline-flex; align-items: center; gap: 6px; padding: 6px 10px; cursor: pointer;
  border: 1px solid rgba(212, 160, 23, .25); border-radius: 999px; font-size: 14px;
}
.custom-pick img { width: 26px; height: 26px; padding: 3px; }
.custom-pick input { accent-color: var(--gold-bright, #d4a017); }
.custom-getlist {
  border: 1px solid rgba(212, 160, 23, .3); border-radius: 8px;
  padding: 12px 14px; background: rgba(20, 15, 8, .3); margin-bottom: 6px;
}
.custom-getrow {
  display: grid; grid-template-columns: 32px 1fr auto; gap: 10px; align-items: center;
  padding: 5px 0; border-bottom: 1px solid rgba(212, 160, 23, .12);
}
.custom-getrow:last-child { border-bottom: none; }
.custom-getrow img { width: 30px; height: 30px; padding: 3px; }

/* Custom: the budget box is a plain figure, not a stepper — the spinner arrows
   invited clicking through a price one cent at a time (Aug 7 2026). */
.custom-budget-input input::-webkit-outer-spin-button,
.custom-budget-input input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
.custom-budget-input input { -moz-appearance: textfield; appearance: textfield; }
.custom-unspent { margin-top: 10px; font-size: 13px; line-height: 1.45; }

/* ── number fields, Aug 9 2026 ──────────────────────────────────────────────
   "everywhere where there is a number you can put and there is arrow up and
   down - remove it to just field where I can put a number and arrow left and
   right." The native spinner disappears from EVERY number field in one rule;
   app.js wraps each bare field in the carved left/right steppers the Army
   cards introduced (.qty-tri). The admin table's cell editor keeps its plain
   field (no steppers), but loses the spinner like everything else. */
input[type=number]::-webkit-outer-spin-button,
input[type=number]::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
input[type=number] { -moz-appearance: textfield; appearance: textfield; }

.qty-tri { display: inline-flex; align-items: center; gap: 5px; vertical-align: middle; max-width: 100%; }
/* Smaller than the Army cards' 34px — these sit inside table rows and modal
   forms where 34px arrows would shout. */
.qty-tri .qty-step { width: 26px; height: 26px; }
.qty-tri input[type=number] { text-align: center; min-width: 0; }
/* The Custom builder's manual rows put the field in a fixed grid column; let
   the tri fill it and the field flex inside between the arrows. */
.custom-row .qty-tri { width: 100%; }
.custom-row .qty-tri input[type=number] { flex: 1 1 auto; width: auto; }

/* ── the Custom builder wears the shop's wooden frame ──────────────────────
   Same 9-slice as the item cards (.cards > .card), same glow, same ink
   colours re-tuned for dark wood instead of parchment. */
.custom-inline.wood-box {
  border-style: solid; border-color: transparent;
  border-width: 22px 19px 22px 19px;
  border-image: url('/assets/ui/frame_wood.png?v=32') 68 116 70 118 fill / 22px 19px 22px 19px stretch;
  background: none; border-radius: 0;
  padding: 10px 8px 14px;
  box-shadow: 0 0 18px rgba(212, 160, 23, .20);
  color: #f2e8d0;
  --backdrop: 58, 38, 22;
}
.custom-inline.wood-box h2 { color: #f7edd4; text-shadow: 0 1px 3px rgba(0, 0, 0, .7); }
.custom-inline.wood-box .muted { color: #d9c9a6; }
.custom-inline.wood-box .label-caps { color: #e8d9b0; }
.custom-inline.wood-box .custom-row-name { color: #f2e8d0; }
/* Inputs stay light-surfaced so typed numbers keep their parchment contrast. */
.custom-inline.wood-box input[type=number] {
  background: var(--surface-cont); color: var(--on-surface);
  border: 1px solid var(--outline-var); border-radius: 4px; padding: 6px 4px;
}
/* The manual rows' own `.custom-row img` (40px, for resource icons) also hits
   the arrow art inside the steppers and spills it out of its 26px button as a
   pale halo. Appended last so it outranks that rule for stepper images only. */
.qty-tri .qty-step img { width: 100%; height: 100%; }

/* ── the personal cabinet (#/account), Aug 9 2026 ──────────────────────────
   "Make account page more user friendly like personal cabinet." The page now
   opens on a wood-framed hero — crest, name, league progress, wallet — with a
   settings LIST underneath (value and action on the same row) instead of a
   fact grid with a detached pile of buttons. */
.cab-hero {
  display: flex; align-items: center; gap: 22px;
  border-style: solid; border-color: transparent;
  border-width: 22px 19px 22px 19px;
  border-image: url('/assets/ui/frame_wood.png?v=32') 68 116 70 118 fill / 22px 19px 22px 19px stretch;
  padding: 16px 14px; margin-bottom: 18px;
  box-shadow: 0 0 18px rgba(212, 160, 23, .20);
  color: #f2e8d0;
  --backdrop: 58, 38, 22;
}
.cab-crest { width: 96px; height: 96px; object-fit: contain; flex-shrink: 0;
  filter: drop-shadow(0 3px 6px rgba(0, 0, 0, .5)); }
.cab-hero-main { flex: 1 1 auto; min-width: 220px; }
.cab-name { margin: 0 0 4px; color: #f7edd4; text-shadow: 0 1px 3px rgba(0, 0, 0, .7); font-size: 28px; }
.cab-standing { color: #e8d9b0; }
.cab-since { color: #cdbb92; }
.cab-progress {
  height: 12px; border-radius: 6px; margin-top: 12px;
  background: rgba(0, 0, 0, .45);
  border: 1px solid rgba(232, 190, 70, .35);
  overflow: hidden;
}
.cab-progress-fill {
  height: 100%; border-radius: 6px 0 0 6px;
  background: linear-gradient(180deg, #f0cf7a, #d4a017 60%, #a87b10);
  box-shadow: 0 0 8px rgba(232, 190, 70, .55);
  transition: width .4s ease;
}
.cab-progress-note { margin-top: 6px; font-size: 13px; color: #d9c9a6; }
.cab-progress-note b { color: #f0cf7a; }
.cab-hero-side {
  display: flex; flex-direction: column; align-items: center; gap: 8px;
  flex-shrink: 0; min-width: 150px; text-align: center;
}
.cab-wallet { display: flex; align-items: center; gap: 8px; font-size: 20px; color: #f7edd4; }
.cab-wallet img { width: 34px; height: 34px; }
.cab-wallet span { font-size: 12px; letter-spacing: .08em; text-transform: uppercase; color: #d9c9a6; }
.cab-pass { font-size: 13px; color: #cdbb92; }
.cab-pass.is-on { color: #b8e0a0; }
.cab-pass b { color: #d6f2c2; }

.cab-cols { display: grid; grid-template-columns: 1.25fr 1fr; gap: 16px; align-items: start; }
@media (max-width: 900px) { .cab-cols { grid-template-columns: 1fr; }
  .cab-hero { flex-wrap: wrap; } }
.cab-card h2 { display: flex; align-items: baseline; gap: 8px; }
.cab-count { font-size: 15px; font-weight: 400; }

.cab-rows { display: flex; flex-direction: column; }
.cab-row {
  display: flex; align-items: center; gap: 14px;
  padding: 12px 2px;
  border-bottom: 1px solid var(--outline-var);
}
.cab-row:last-child { border-bottom: none; }
.cab-row-label { flex: 0 0 130px; }
.cab-row-value { flex: 1 1 auto; min-width: 0; overflow-wrap: anywhere; }
.cab-row-actions { display: flex; gap: 8px; flex-shrink: 0; flex-wrap: wrap; justify-content: flex-end; }
.cab-amb-ico { width: 22px; height: 22px; }

/* ── landing polish, Aug 9 2026 ─────────────────────────────────────────────
   "Make it more presentable. Work on the flow." The sign-in card was a plain
   white sheet floating over the battlefield; now it is a wooden shrine like
   the rest of the game's chrome, and after the last feature band a closing
   plank actually invites the visitor in instead of letting the page trail off. */
.landing-card.panel.marble {
  background: none; border-radius: 0;
  border-style: solid; border-color: transparent;
  border-width: 22px 19px 22px 19px;
  border-image: url('/assets/ui/frame_wood.png?v=32') 68 116 70 118 fill / 22px 19px 22px 19px stretch;
  box-shadow: 0 10px 34px rgba(0, 0, 0, .55), 0 0 18px rgba(212, 160, 23, .18);
  color: #f2e8d0;
  --backdrop: 58, 38, 22;
}
.landing-card .landing-tabs { justify-content: center; }
.landing-card .landing-tab {
  background: rgba(0, 0, 0, .3); color: #e8d9b0;
  border: 1px solid rgba(232, 190, 70, .3); border-radius: 4px;
}
.landing-card .landing-tab.on {
  background: linear-gradient(180deg, #f0cf7a, #d4a017 65%, #a87b10);
  color: #2a1c08; border-color: #a87b10; font-weight: 700;
  box-shadow: 0 0 10px rgba(232, 190, 70, .4);
}
.landing-card .landing-field > span { color: #e8d9b0; }
.landing-card .landing-field input, .landing-card .landing-field select {
  background: var(--surface-cont); color: var(--on-surface);
  border: 1px solid rgba(0, 0, 0, .4);
}
.landing-card .landing-check { color: #d9c9a6; }
.landing-card .landing-check a, .landing-card .landing-terms a { color: #f0cf7a; }
.landing-card .landing-terms { color: #cdbb92; }
.landing-card .landing-err { color: #ffb0a3; }
.landing-card .linklike { color: #f0cf7a; }

.landing-cta {
  text-align: center; padding: 64px 20px 72px; max-width: 620px; margin: 0 auto;
}
.landing-cta h2 { font-family: var(--serif); font-size: 30px; margin: 0 0 10px; }
.landing-cta p { color: var(--on-surface-var); margin: 0 0 22px; }
.landing-cta-btn { font-size: 17px; padding: 16px 34px; }

/* ── legal pages: contract text that reads like a document ─────────────────── */
.legal-layout { display: grid; grid-template-columns: 210px 1fr; gap: 34px; align-items: start; }
.legal-toc { position: sticky; top: 86px; display: flex; flex-direction: column; gap: 2px; }
.legal-toc-title { margin-bottom: 6px; color: var(--on-surface-var); }
.legal-toc a {
  padding: 5px 10px; border-left: 2px solid var(--outline-var);
  color: var(--on-surface-var); text-decoration: none; font-size: 14px;
  cursor: pointer;
}
.legal-toc a:hover { color: var(--on-surface); border-left-color: var(--gold); }
.legal-body { max-width: 660px; }
.legal-body h2 {
  font-family: var(--serif); font-size: 22px; margin: 34px 0 10px;
  padding-bottom: 6px; border-bottom: 1px solid rgba(212, 160, 23, .45);
  scroll-margin-top: 86px;
}
.legal-body h2:first-child { margin-top: 0; }
.legal-body p { line-height: 1.7; margin: 0 0 14px; }
.legal-body ul { margin: 0 0 14px; padding-left: 22px; }
.legal-body li { line-height: 1.65; margin-bottom: 7px; }
.legal-updated { font-size: 13px; font-style: italic; }
@media (max-width: 900px) { .legal-layout { grid-template-columns: 1fr; } .legal-toc { position: static; flex-direction: row; flex-wrap: wrap; gap: 6px; } .legal-toc-title { width: 100%; } .legal-toc a { border-left: none; border: 1px solid var(--outline-var); border-radius: 4px; } }

/* ── admin dashboard: quick-find and 14-day trends (Aug 9 2026) ───────────── */
.admin-quickfind { display: flex; gap: 8px; margin-bottom: 14px; max-width: 460px; }
.admin-quickfind input {
  flex: 1 1 auto; padding: 9px 12px; border: 1px solid var(--outline-var);
  border-radius: 6px; background: #fff; font-size: 14px;
}
.admin-quickfind input:focus { outline: none; border-color: var(--primary); box-shadow: 0 0 0 3px rgba(155, 89, 24, .15); }
.admin-trends { display: grid; grid-template-columns: repeat(auto-fit, minmax(230px, 1fr)); gap: 12px; margin-top: 16px; }
.admin-trend { border: 1px solid var(--outline-var); border-radius: 8px; padding: 12px 14px; background: #fdfcf9; }
.admin-trend-head { display: flex; align-items: baseline; gap: 8px; margin-bottom: 8px; }
.admin-trend-head b { font-size: 17px; }
.admin-trend-bars { display: flex; align-items: flex-end; gap: 3px; height: 56px; }
.admin-trend-bar { flex: 1 1 0; height: 100%; display: flex; align-items: flex-end; }
.admin-trend-bar i {
  display: block; width: 100%; border-radius: 2px 2px 0 0;
  background: linear-gradient(180deg, #c98a3d, #9b5918);
}
.admin-trend-bar.is-zero i { background: var(--outline-var); }
.admin-trend-bar:hover i { filter: brightness(1.15); }
/* The closing plank's headline inherits the hero's pale ink, which vanishes on
   the parchment page background — pin it to page ink. */
.landing-cta h2 { color: var(--on-surface); }

/* battle top queue + outcome icons (Aug 9 2026) */
.init-chip.spent { opacity: .38; filter: grayscale(.5); }
.init-chip.current { box-shadow: 0 0 0 2px var(--gold-bright); transform: scale(1.06); }
.outcome-ico { width: 22px; height: 22px; vertical-align: -5px; }

/* admin Pages tab (Aug 9 2026) — big editor + live preview side by side */
.admin-page-edit { display: grid; grid-template-columns: 1fr 1fr; gap: 18px; align-items: start; }
@media (max-width: 1000px) { .admin-page-edit { grid-template-columns: 1fr; } }
.admin-page-text { width: 100%; font: 13px/1.55 ui-monospace, monospace; padding: 12px; border: 1px solid var(--outline-var); border-radius: 8px; resize: vertical; }
.admin-page-hints { font-size: 12px; margin-top: 6px; }
.admin-page-hints code { background: #f1ece2; padding: 0 4px; border-radius: 3px; }
.admin-page-preview { border: 1px dashed var(--outline-var); border-radius: 8px; padding: 6px 16px; max-height: 420px; overflow-y: auto; background: #fdfcf9; }
.admin-page-preview h3 { border-bottom: 1px solid rgba(212, 160, 23, .45); padding-bottom: 4px; }

/* landing feature bands as real gameplay captures (Aug 9 2026) */
/* Aug 12 2026 — "Use actual photos of agonsagaonline account for showcase, no
   need to add anything there."

   The gilt border, the rounded corners and the 16:10 crop all came off. The
   captures are 16:9 and the crop was quietly cutting a strip off the top and
   bottom of every one of them; the board around the band is frame enough. */
.landing-art-shot img {
  width: 100%; height: auto; display: block;
  border: none; border-radius: 0;
  aspect-ratio: auto;
  box-shadow: 0 10px 26px rgba(0, 0, 0, .5);
}
.landing-light .landing-art-shot img { box-shadow: 0 10px 26px rgba(0, 0, 0, .45); }

/* ── the battle report rides a scroll (Aug 9 2026) ───────────────────────── */
.battle-report {
  border: solid transparent;
  border-width: 100px 92px 102px 92px;
  border-image: url('/assets/ui/scroll_frame.png?v=38') 175 160 175 160 fill / 100px 92px 102px 92px stretch;
  max-width: 560px; margin: -20px -10px;
}
.battle-report-inner { color: #2a2013; --backdrop: 232, 220, 194; padding: 0 10px; }
.battle-report-inner .muted { color: #6d5c42; }
/* The marble modal box steps aside — the scroll IS the frame. */
.modal:has(.battle-report) {
  background: none !important; border: none !important;
  box-shadow: none !important; padding: 0;
}
.modal:has(.battle-report) .close { top: 66px; right: 66px; }

/* ── the battle frame, after the user's HoMM reference (Aug 9 2026) ───────── */
.battle-field { display: flex; flex-direction: column; min-width: 0; }
.battle-bottombar {
  display: flex; align-items: stretch; gap: 8px;
  margin-top: 6px; padding: 6px 8px;
  background: linear-gradient(180deg, #4a3522, #38281a);
  border: 2px solid #7a5c33; border-radius: 6px;
}
.bb-side { display: flex; gap: 5px; flex: 1 1 0; min-width: 0; overflow-x: auto; }
.bb-side.bb-def { justify-content: flex-end; }
.bb-cell {
  display: flex; flex-direction: column; align-items: center; gap: 1px;
  min-width: 44px; padding: 3px 4px 2px;
  background: rgba(0, 0, 0, .35); border: 1px solid #8a6a3c; border-radius: 4px;
}
.bb-cell img { width: 26px; height: 26px; object-fit: contain; }
.bb-cell b { font-size: 12px; color: #f0e2c4; line-height: 1; }
.bb-cell.current { border-color: #f0cf7a; box-shadow: 0 0 8px rgba(232, 190, 70, .65); background: rgba(212, 160, 23, .25); }
.bb-round {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  padding: 0 14px; border-left: 1px solid #7a5c33; border-right: 1px solid #7a5c33;
  color: #d9c9a6; flex-shrink: 0;
}
.bb-round b { font-size: 20px; color: #f0cf7a; line-height: 1; }

/* the Chronicle rides the parchment scroll, build-tray scale */
.battle-log-scroll {
  border: solid transparent;
  border-width: 44px 32px 46px 32px;
  border-image: url('/assets/ui/scroll_frame.png?v=38') 175 160 175 160 fill / 44px 32px 46px 32px stretch;
}
.battle-log-scroll-inner { color: #2a2013; --backdrop: 232, 220, 194; margin: -14px -4px; }

/* ── fullscreen battle, HoMM-style (Aug 9 2026) ───────────────────────────── */
.battle-page {
  display: flex; flex-direction: column;
  height: 100vh; box-sizing: border-box;
  /* Round 43 — the frame is a real picture frame and its border claims a
     quarter of the picture on every side, so every pixel of page chrome comes
     out of the battlefield. The resource bar floats over the middle-top only;
     the field is centred BELOW it, so the page needs far less headroom than
     the bar's full height, and the topline overlays rather than stacking. */
  padding: 96px 10px 6px;
  background: radial-gradient(ellipse at 50% 30%, #2b2218, #15100b 75%);
}
@media (max-width: 1100px) { .battle-page { padding-top: 84px; } }
.battle-topline {
  position: absolute; left: 12px; right: 12px; top: 96px;
  display: flex; align-items: center; justify-content: space-between;
  gap: 10px; pointer-events: none; z-index: 3;
}
.battle-topline > * { pointer-events: auto; }
.battle-topline .init-bar { flex: 1 1 auto; margin-bottom: 0; }
.battle-stage {
  flex: 1 1 auto; min-height: 0; position: relative;
  display: flex; align-items: center; justify-content: center;
}
.battle-stage canvas {
  max-width: 100%; max-height: 100%;
  border-radius: 8px; box-shadow: 0 12px 40px rgba(0, 0, 0, .6);
}
/* the framed picture takes whatever the stage has */
.battle-stage .battle-map-frame { max-width: 100%; max-height: 100%; }
.battle-chron {
  position: absolute; right: 8px; bottom: 8px; width: min(400px, 90%);
  max-height: 60%; overflow: hidden; z-index: 4;
}
.battle-chron .battle-log { max-height: 32vh; }
/* Round 43 — the strip between field and bottom bar is gone. Its two
   survivors: the favours, which moved in with the other actions, and the
   aiming hint, which floats over the field only while a favour is pointed. */
.battle-hint {
  position: absolute; left: 50%; bottom: 14px; transform: translateX(-50%);
  padding: 6px 16px; border-radius: 999px; pointer-events: none;
  background: rgba(24, 16, 9, .78); color: #ffe9a8;
  font-weight: 700; letter-spacing: .02em;
  text-shadow: 0 1px 3px rgba(0, 0, 0, .8);
  opacity: 0; transition: opacity .18s ease; z-index: 4;
}
.battle-hint:not(:empty) { opacity: 1; }
.battle-round { color: #e7d8b5; text-shadow: 0 2px 5px rgba(0, 0, 0, .8); align-self: center; margin-right: auto; }
.hb-actions .favor-bar { display: flex; gap: 6px; flex-wrap: wrap; margin-left: 6px; }
.hb-actions { display: flex; align-items: center; gap: 6px; flex-shrink: 0; flex-wrap: wrap; }
.battle-page .init-chip { background: rgba(0, 0, 0, .4); border-color: #8a6a3c; color: #f0e2c4; }
.battle-page .init-chip.att { background: rgba(28, 99, 144, .3); }
.battle-page .init-chip.def { background: rgba(147, 0, 10, .3); }
@media (max-width: 900px) {
  .battle-page { height: auto; min-height: 100vh; }
  .battle-bottombar { flex-wrap: wrap; }
}
/* The battle is its own room — the game's bottom nav would sit on top of the
   battle bar, so it steps out while a battle page is open. */
body:has(.battle-page) .bottom-nav { display: none; }
/* the plank's battle icon is an icon, not a mural */
.report-plank { display: flex; align-items: center; justify-content: center; gap: 12px; }
.report-plank-ico { width: 38px; height: 38px; object-fit: contain; filter: drop-shadow(0 2px 3px rgba(0,0,0,.5)); }

/* ── the attack dossier (Aug 9 2026) ──────────────────────────────────────── */
/* Aug 12 2026 — "make so when you are pressing start 1st mission in troy - it
   warns you that you are not ready for it." The warning is the first thing in
   the confirm dialog and it is not a muted hint: a player about to lose an
   army should have to read past it. */
.not-ready {
  border: 1px solid #c2662a; border-left-width: 5px; border-radius: 6px;
  background: rgba(196, 96, 26, .1);
  padding: 12px 16px; margin-bottom: 14px;
}
.not-ready-title { display: block; color: #a8471a; font-size: 1.02rem; margin-bottom: 5px; }
.not-ready p { margin: 0; color: var(--on-surface-var); line-height: 1.55; font-size: .93rem; }

.attack-brief { display: flex; flex-direction: column; gap: 12px; margin-bottom: 14px; }
.ab-row { display: flex; align-items: center; gap: 12px; }
.ab-row > .label-caps { flex: 0 0 110px; }
.ab-units { display: flex; gap: 6px; flex-wrap: wrap; }
.ab-unit {
  position: relative; width: 40px; height: 40px; border-radius: 50%;
  border: 2px solid #b08f4f; overflow: hidden; background: #f4ecd9;
  box-shadow: 0 2px 5px rgba(0, 0, 0, .25);
}
.ab-unit img { width: 100%; height: 100%; object-fit: cover; }
.ab-unit i {
  position: absolute; right: 0; bottom: 0; font-style: normal; font-weight: 800;
  font-size: 11px; background: rgba(43, 30, 12, .85); color: #f0e2c4;
  padding: 0 4px; border-radius: 5px 0 0 0;
}
.ab-odds { display: flex; align-items: center; gap: 10px; flex: 1; }
.ab-meter {
  flex: 0 0 120px; height: 10px; border-radius: 5px;
  background: rgba(0, 0, 0, .25); border: 1px solid rgba(90, 66, 34, .45);
  overflow: hidden;
}
.ab-meter i { display: block; height: 100%; border-radius: 5px 0 0 5px; }
.ab-odds[data-odds=desperate] i { background: #a02a1e; }
.ab-odds[data-odds=uphill] i { background: #c07f2a; }
.ab-odds[data-odds=even] i { background: #c9b23c; }
.ab-odds[data-odds=favoured] i { background: #7ba03c; }
.ab-odds[data-odds=sure] i { background: #4d8a3d; }
.ab-odds b { color: #43301a; }
.ab-loots { display: flex; gap: 8px; flex-wrap: wrap; }
.ab-loot {
  display: inline-flex; align-items: center; gap: 5px;
  padding: 3px 10px; border-radius: 999px; font-weight: 700; font-size: 13px;
  background: rgba(212, 160, 23, .18); border: 1px solid var(--gold); color: #5c430e;
}
.ab-loot img { width: 18px; height: 18px; }

/* Aug 10 2026 — "In battle report - please fix so the scroll is all the time."
   Two things made the scroll vanish: the frame PNG was requested under two
   different ?v= strings (v=27 and v=32 — two 768 KB downloads, one of them
   always cold), and until the image arrived the border-image painted NOTHING,
   so the reports floated naked on the battlefield. Versions are unified to
   v=38, index.html preloads the frame, and every scroll panel now carries a
   plain-parchment fallback clipped to the padding box — the opaque middle of
   the loaded 9-slice covers it exactly, so once the art lands nothing changes,
   but there is never a frame-less flash again. */
.modal, .raid-page .raid-history, .k-scroll, .battle-report, .battle-log-scroll {
  background: #ece0c6 !important;
  background-clip: padding-box !important;
}
/* The reports scroll also stretched edge-to-edge on wide screens, which pulled
   the rows miles from the rollers. Cap and center it like the Koinon scrolls. */
.raid-page .raid-history {
  width: min(1100px, 100%);
  align-self: center;
}

/* Aug 10 2026 — the friendlier Garrison menu: live slot tally with pips, All/
   None shortcuts, rows light up while they hold troops. */
.gar-config .gar-slotbar {
  display: flex; align-items: center; gap: 8px;
  padding: 8px 12px; border-radius: 8px;
  background: rgba(90, 66, 34, .10); border: 1px solid rgba(90, 66, 34, .28);
}
.gar-config .gar-slotbar b { font-size: 17px; color: #2a2013; }
.gar-slot-pips { display: inline-flex; gap: 4px; margin-left: 4px; }
.gar-pip {
  width: 12px; height: 12px; border-radius: 50%;
  border: 1px solid rgba(90, 66, 34, .45); background: rgba(255, 251, 240, .5);
}
.gar-pip.on { background: var(--gold); border-color: #8a5f14; box-shadow: 0 0 4px rgba(212, 160, 23, .5); }
.gar-slot-warn { color: #8c1616; font-size: 13px; font-weight: 700; margin-left: auto; }
.gar-config .gar-row { align-items: center; gap: 10px; }
.gar-config .gar-row.gar-row-on { background: rgba(212, 160, 23, .12); border-radius: 8px; }
.gar-config .gar-row-sub { font-size: 12px; }
.gar-config .btn-plaque[disabled] { filter: grayscale(.7) brightness(.8); cursor: not-allowed; }

/* Aug 10 2026 — "make Battle History clicable menu after what the scroll
   opens": the plank is a button now. Same wood, plus hands-on affordances. */
button.report-plank-btn {
  background: none; cursor: pointer;
  font: inherit; font-size: 1.25rem; font-weight: 700; font-family: inherit;
  transition: transform .12s ease, filter .12s ease;
}
button.report-plank-btn:hover { transform: translateY(-2px) scale(1.02); filter: brightness(1.12); }
button.report-plank-btn:active { transform: translateY(0) scale(.99); }

/* the history rides the modal scroll; same parchment ink the inline list wore */
.raid-history-modal { color: #2a2013; --backdrop: 232, 220, 194; min-width: min(680px, 72vw); }
.raid-history-modal h2 { color: #2a2013; }
.raid-history-modal .raid-reports { max-height: 56vh; overflow-y: auto; min-height: 120px; padding-right: 6px; }
.raid-history-modal .raid-report {
  background: rgba(90, 66, 34, .08);
  border-color: rgba(90, 66, 34, .3); color: #2a2013;
}
.raid-history-modal .raid-report b { color: #1d1408; }
.raid-history-modal .raid-report .muted,
.raid-history-modal .raid-report-age { color: #6d5c42; }
.raid-history-modal .raid-reports > .muted { color: #6d5c42; }
.raid-history-modal .raid-chip {
  background: rgba(255, 251, 240, .65); border-color: rgba(90, 66, 34, .35); color: #43301a;
}
.raid-history-modal .raid-chip-loot { background: rgba(212, 160, 23, .25); border-color: var(--gold); color: #5c430e; }
.raid-history-modal .raid-chip-up { background: rgba(92, 138, 58, .22); border-color: #5c8a3a; color: #3d6127; }
.raid-history-modal .raid-chip-down { background: rgba(186, 26, 26, .14); border-color: var(--error); color: #8c1616; }
.gar-sep { flex: 0 0 1px; height: 18px; background: rgba(90, 66, 34, .35); margin: 0 4px; }

/* Aug 10 2026 — the raid dossier round: open-field preview, price on the
   re-roll, iconified march cost in the picker's foot. */
.rt-openfield { height: 170px; }
.rt-openfield .rv-lineup { bottom: 26px; gap: 14px; }
.rt-openfield .rv-unit { width: 56px; height: 56px; }
.rt-glory-mine { display: block; font-size: 12px; }
.raid-glory { text-align: right; }
.rt-brief { margin-top: 4px; }
.rt-price { display: inline-flex; gap: 6px; margin-left: 8px; }
.rt-price .cost { font-size: 13px; }
.picker-foot { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; }
.picker-cost { display: inline-flex; align-items: center; gap: 8px; margin-left: auto; }

/* Aug 10 2026 — "Make training bar thinner": the 20px liquid-gold bar was a
   drink; under a one-line training row it wants to be a sliver. */
.training-row .gold-bar { height: 9px; border-radius: 2px; }
.rt-league { display: inline-flex; align-items: center; gap: 6px; margin-top: 4px;
  font-size: 13px; font-weight: 700; color: #5c430e; }
.rt-league .league-crest.sm { width: 22px; height: 22px; }

/* Aug 10 2026 — the HoMM-style turn strip: ten portraits riding the bottom
   bar, the acting stack lit. Side reads from the frame colour. */
.turn-strip {
  display: flex; justify-content: center; gap: 4px;
  margin: 4px 0 0; min-height: 46px;
}
.ts-cell {
  position: relative; width: 44px; height: 44px;
  background: rgba(0, 0, 0, .45); border: 2px solid #6a4f2c; border-radius: 5px;
  display: flex; align-items: center; justify-content: center;
}
.ts-cell img { width: 34px; height: 34px; object-fit: contain; }
.ts-cell b {
  position: absolute; right: 1px; bottom: 0; font-size: 10px; line-height: 1;
  color: #f0e2c4; background: rgba(0, 0, 0, .65); border-radius: 3px; padding: 1px 3px;
}
/* Round 41 — "make so your units are highlighted with yellow instead of blue
   and active unit has glow around them": your own stacks wear the gold of
   your own banner, the enemy's wear red, and whoever is acting pulses so the
   eye finds it without reading. */
.ts-cell.att {
  border-color: #e0b64a;
  background: linear-gradient(180deg, rgba(212, 160, 23, .28), rgba(0, 0, 0, .45));
}
.ts-cell.def {
  border-color: #b05050;
  background: linear-gradient(180deg, rgba(150, 40, 40, .3), rgba(0, 0, 0, .45));
}
/* The actor keeps its SIDE colour — gold stays "mine", red stays "theirs" —
   and is marked instead by a ring of light around it and a caret above, so a
   glowing enemy can never be mistaken for one of yours. */
.ts-cell.current {
  transform: scale(1.12);
  animation: tsPulse 1.5s ease-in-out infinite;
  outline: 2px solid rgba(255, 233, 168, .95);
  outline-offset: 2px;
  z-index: 2;
}
.ts-cell.current::before {
  content: ''; position: absolute; top: -11px; left: 50%; transform: translateX(-50%);
  border-left: 6px solid transparent; border-right: 6px solid transparent;
  border-top: 7px solid #ffe9a8;
  filter: drop-shadow(0 1px 2px rgba(0, 0, 0, .6));
}
@keyframes tsPulse {
  0%, 100% { box-shadow: 0 0 8px 2px rgba(255, 216, 110, .65), inset 0 0 8px rgba(255, 226, 150, .35); }
  50%      { box-shadow: 0 0 18px 6px rgba(255, 216, 110, .95), inset 0 0 12px rgba(255, 236, 180, .55); }
}
/* a hair of air between the sides so the two hosts read apart */
.battle-bottombar .turn-strip { gap: 6px; padding: 12px 6px 0; align-items: center; flex-wrap: nowrap; min-width: 0; }
.battle-bottombar .ts-cell { flex: 0 1 auto; min-width: 0; }
.ts-cell + .ts-cell { margin-left: 0; }

/* Aug 10 2026 — "battle report is still not on the scroll" (second report,
   from the user's Mac): whatever Safari thinks of the modal box, the history
   now carries its OWN scroll skin — the same inner-element pattern the
   victory report uses, which that browser demonstrably renders. The modal
   chrome steps aside entirely. */
.modal:has(.raid-history-modal) {
  background: none !important; border: none !important;
  box-shadow: none !important; padding: 0; overflow: visible;
}
.raid-history-modal {
  border: solid transparent;
  border-width: 100px 92px 102px 92px;
  border-image: url('/assets/ui/scroll_frame.png?v=38') 175 160 175 160 fill / 100px 92px 102px 92px stretch;
  background: #ece0c6; background-clip: padding-box;
  width: min(760px, 92vw); min-width: 0;
  margin: 0 auto;
  padding: 0 24px;  /* the torn edge wanders inside the border width */
}
.raid-history-modal .raid-reports { max-height: 52vh; }
.modal:has(.raid-history-modal) .close { top: 64px; right: 70px; }

/* "after victory menu looks a bit off": the report scroll hugged the left
   edge (margin: -20px -10px is not a centering margin), and the ✕ floated
   at the far corner of the invisible modal box, nowhere near the scroll.
   Centered, with the ✕ pinned to the scroll's own shoulder. */
.battle-report { margin: -20px auto; }
.modal:has(.battle-report) { align-items: center; }
.modal:has(.battle-report) .close {
  top: 46px;
  right: max(28px, calc(50% - 280px + 34px));
}
/* plunder and glory read as the same icon chips the dossier promises */
.battle-report .ab-loot img { width: 18px; height: 18px; }
.battle-report .br-chips { display: flex; flex-wrap: wrap; gap: 6px; justify-content: center; }

/* Aug 10 2026 round 36 — dossier, bottom bar, victory scroll */

/* "Make bigger Their Host and rename it to Enemy forces" */
.rt-forces-row { align-items: flex-start; }
.rt-forces-row > .label-caps { padding-top: 26px; }

.rt-shield-line { position: static; margin: 0 0 8px; display: inline-block; }

/* "instead of the forces and round number - make only unit attack sequence
   and make it bigger": the strip lives inside the bar now, at HoMM size. */
/* Round 43 — "make so unit queue is not scrollable": ten portraits always
   fit; the strip shrinks them rather than hiding any behind a scrollbar. */
.battle-bottombar .turn-strip { flex: 1 1 auto; margin: 0; min-height: 64px; overflow: visible; }
.battle-bottombar .ts-cell { width: 62px; height: 62px; border-width: 3px; border-radius: 7px; }
.battle-bottombar .ts-cell img { width: 50px; height: 50px; }
.battle-bottombar .ts-cell b { font-size: 12px; padding: 1px 4px; }

/* "After a victory - make so scroll is fixed size and you can't scroll it,
   because it cuts the image": the modal stops capping its height for the
   report, so the whole scroll — rollers included — is simply there. */
.modal:has(.battle-report) { max-height: none; overflow: visible; }
.modal:has(.battle-report) .modal-body { overflow: visible; }
.k-upkeep-free { background: rgba(212, 160, 23, .16); border: 1px solid var(--gold); border-radius: 8px; }
.k-upkeep-free b { color: #5c430e; }
.k-feeset { margin-top: 8px; }

/* Aug 10 2026 round 37 */
/* Round 39 — "Fix displayed amount of enemy troops design - it looks bad."
   The count stopped being a badge stuck on the portrait's chin and became
   part of a proper card: a ringed portrait, the strength on a bronze plate
   beneath it, the unit's name under that. Three lines that read top to
   bottom, so a glance gives you what they are and how many. */
.rt-forces { display: flex; gap: 18px; flex-wrap: wrap; align-items: flex-start; }
.rt-force { display: flex; flex-direction: column; align-items: center; gap: 6px; width: 84px; }
.rt-force-por {
  width: 72px; height: 72px; border-radius: 50%;
  background: radial-gradient(circle at 38% 32%, #fdf6e6, #e6d7b6 78%);
  border: 2px solid #b08f4f;
  box-shadow: 0 2px 6px rgba(60, 42, 16, .3), inset 0 0 0 3px rgba(255, 253, 245, .55);
  display: flex; align-items: center; justify-content: center; overflow: hidden;
}
.rt-force-por img { width: 84%; height: 84%; object-fit: contain; display: block; }
.rt-force-count {
  min-width: 58px; text-align: center; white-space: nowrap;
  padding: 2px 10px; border-radius: 999px;
  background: linear-gradient(180deg, #6b4f22, #4a3415);
  border: 1px solid #a5813c; color: #ffdf9b;
  font-size: 13px; font-weight: 800; letter-spacing: .02em;
  box-shadow: 0 1px 3px rgba(40, 28, 10, .45);
}
.rt-force-name {
  font-size: 11px; letter-spacing: .06em; text-transform: uppercase;
  color: #6d5c42; text-align: center; line-height: 1.2;
}
/* the picker's confirm keeps the compact corner badge */
.ab-unit .ab-band {
  position: absolute; right: 0; bottom: 0; font-style: normal;
  background: rgba(43, 30, 12, .85); color: #f0e2c4;
  padding: 0 4px; border-radius: 5px 0 0 0; font-size: 10px; font-weight: 800;
}
/* icon chips in the battle history rows */
.raid-chip .chip-ico { width: 16px; height: 16px; object-fit: contain; vertical-align: -3px; margin-right: 2px; }

/* Aug 10 2026 — "frame for battle map and background for battle window":
   the map wears the user's wood-and-verdigris frame; the storm video fills
   the window behind everything, paused until a stack falls. */
/* Round 38 — "use another frame that is in the folder": the marble-and-gilt
   square frame replaces the wooden one. No `fill` in the 9-slice, so the
   frame's own middle is never painted over the battlefield. */
/* Round 43 — "fix the frame ... it looks not as I gave it to you". Both
   9-slice attempts were doomed: `stretch` smeared the carving across a wide
   map, `round` tiled it into repeats the original does not have. The frame is
   now drawn ONCE, whole, scaled like the picture frame it is, with the canvas
   seated in its opening — so what renders is the file as drawn, corner
   ornaments and all. The padding that seats it is computed from the measured
   art in fitCanvas(). */
.battle-map-frame {
  display: inline-block; line-height: 0;
  background: url('/assets/ui/battle_map_frame3.png?v=43') center / 100% 100% no-repeat;
  border: none; padding: 42px;      /* exact values applied on layout */
  filter: drop-shadow(0 10px 26px rgba(0, 0, 0, .5));
}
/* the canvas IS the picture inside the frame — its own rounding and shadow
   would read as a second object sitting on the art */
.battle-map-frame canvas { display: block; border-radius: 0; box-shadow: none; }
.battle-window-bg, .battle-video-bg {
  position: fixed; inset: 0; width: 100vw; height: 100vh;
  object-fit: cover; z-index: 0; pointer-events: none;
  filter: brightness(.82);
}
/* the still sky is always there; the storm fades over it only while it runs */
.battle-video-bg { opacity: 0; transition: opacity .45s ease; }
.battle-video-bg.is-playing { opacity: 1; }
.battle-page > :not(.battle-video-bg):not(.battle-window-bg) { position: relative; z-index: 1; }
/* Round 38 — "Remove frame on the bottom": the carved plank came off; the
   controls sit on a plain dark rail again so nothing competes with the map
   frame above it. */
.battle-bottombar {
  border: none; border-top: 1px solid rgba(122, 92, 51, .5);
  background: rgba(24, 16, 9, .72);
  border-radius: 0; padding: 6px 10px;
  backdrop-filter: blur(2px);
}
/* the count band must hang below the circular icon, not be clipped by it */
.ab-unit { overflow: visible; }
.ab-unit img { border-radius: 50%; }



/* Round 39 audit — the victory scroll is fixed and unscrollable, so on a
   short laptop screen it must SHRINK rather than run off the edge. */
@media (max-height: 780px) {
  .battle-report {
    border-width: 76px 70px 78px 70px;
    border-image-width: 76px 70px 78px 70px;
    max-width: 470px;
  }
  .battle-report-inner { font-size: .92rem; }
}
@media (max-height: 640px) {
  .battle-report {
    border-width: 58px 54px 60px 54px;
    border-image-width: 58px 54px 60px 54px;
    max-width: 420px;
  }
}

/* Round 41 — the two prices of belonging, on the shortlist and in the hall */
.k-offer-dues { margin-top: 4px; font-size: 12px; display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
.k-offer-dues .cost { font-size: 12px; }
.k-dues-line { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.k-found-prices { display: flex; gap: 18px; flex-wrap: wrap; margin-bottom: 10px; }
.k-found-prices label { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }

/* Round 42 — "instead of glory +42 in green thing it is +42 and icon near,
   without green frame": the number stands on its own, with the trophy beside
   it, rather than boxed in a status pill. */
.br-glory { display: flex; align-items: center; justify-content: center; gap: 8px; }
.br-glory-val { font-size: 1.15rem; font-weight: 800; color: #3d6127; }
.br-glory-val.is-loss { color: #8c1616; }
.br-glory .glyph { vertical-align: middle; }

/* ─────────────────────────────────────────────────────────────────────────
   Aug 11 2026 — "While you were away": the offline-raid dispatch.
   Lykos of the Guard delivers the tally on the same scroll the battle
   history uses (.raid-history-modal supplies the frame; this only styles
   what sits inside it).
   ───────────────────────────────────────────────────────────────────────── */
.raid-planks { display: flex; flex-direction: column; align-items: stretch; }
/* The unread count rides beside the label as a flex child rather than pinned
   to the plank's corner: the plank centres its contents, so an absolutely
   positioned badge lands on top of the last letter of a long title. */
.plank-badge {
  min-width: 24px; height: 24px; padding: 0 7px;
  display: inline-flex; align-items: center; justify-content: center;
  border-radius: 12px; background: var(--error, #ba1a1a);
  border: 1px solid rgba(255, 240, 210, .85);
  color: #fff; font-size: 13px; font-weight: 800; line-height: 1;
  text-shadow: none; box-shadow: 0 2px 6px rgba(0, 0, 0, .55);
}
/* `hidden` loses to a display rule of its own — say it explicitly, or the
   badge stays on screen with nothing in it. */
.plank-badge[hidden] { display: none; }

.while-away-modal .wa-body { max-height: 56vh; overflow-y: auto; padding-right: 6px; }

/* The Spartan speaking. The portrait is the anchor of the whole scroll, so it
   gets real size — this is the one place in the game a character delivers bad
   news, and a thumbnail would undersell it. */
.wa-speech { display: flex; gap: 14px; align-items: flex-start; margin-bottom: 12px; }
.wa-face { width: 104px; height: 104px; flex: 0 0 104px; }
.wa-speech-text { flex: 1; min-width: 0; }
.wa-who { color: #6d4d18; letter-spacing: .08em; }
.wa-speech-text p { margin: 4px 0 0; color: #2a2013; font-size: 15px; line-height: 1.45; }

.wa-total {
  border-top: 1px solid rgba(90, 66, 34, .3);
  border-bottom: 1px solid rgba(90, 66, 34, .3);
  padding: 8px 0; margin: 10px 0 12px;
}
.wa-row { display: flex; align-items: baseline; gap: 6px; margin: 3px 0; }
.wa-key {
  flex: 0 0 88px; font-size: 11px; font-weight: 800; letter-spacing: .08em;
  text-transform: uppercase; color: #6d5c42;
}
/* Chips wrap INSIDE their own column, so a fifth resource lines up under the
   first one instead of jumping back under the label. */
.wa-vals { flex: 1; min-width: 0; display: flex; flex-wrap: wrap; align-items: center; gap: 6px; }
.wa-row .muted { color: #7a6a50; font-size: 13px; }

.wa-chip {
  display: inline-flex; align-items: center; gap: 4px;
  padding: 2px 8px; border-radius: 10px;
  background: rgba(212, 160, 23, .22); border: 1px solid rgba(90, 66, 34, .35);
  color: #5c430e; font-weight: 700; font-size: 13px;
}
.wa-chip-loss { background: rgba(186, 26, 26, .14); border-color: rgba(140, 22, 22, .45); color: #8c1616; }
.wa-chip .chip-ico { width: 18px; height: 18px; }

.wa-list { display: flex; flex-direction: column; gap: 8px; }
.wa-raid {
  display: flex; gap: 10px; align-items: flex-start;
  padding: 8px 10px; border-radius: 6px;
  background: rgba(90, 66, 34, .08);
  border: 1px solid rgba(90, 66, 34, .3);
  border-left-width: 4px;
  color: #2a2013;
}
.wa-raid.is-held { border-left-color: #5c8a3a; }
.wa-raid.is-sacked { border-left-color: var(--error, #ba1a1a); }
.wa-raid.is-old { opacity: .78; }
.wa-raid-icon { width: 34px; height: 34px; flex: 0 0 34px; margin-top: 2px; }
.wa-raid-main { flex: 1; min-width: 0; }
.wa-raid-title { display: flex; flex-wrap: wrap; align-items: baseline; gap: 6px; }
.wa-raid-title b { color: #1d1408; }
.wa-raid-title .muted { color: #6d5c42; font-size: 12px; }
.wa-verdict {
  font-size: 11px; font-weight: 800; letter-spacing: .06em; text-transform: uppercase;
}
.wa-raid.is-held .wa-verdict { color: #3d6127; }
.wa-raid.is-sacked .wa-verdict { color: #8c1616; }
.wa-raid-rows { margin-top: 4px; }
.wa-raid-rows .wa-key { flex-basis: 74px; font-size: 10px; }
.wa-raid-side { text-align: right; flex: 0 0 auto; display: flex; flex-direction: column; gap: 4px; align-items: flex-end; }
.wa-age { color: #6d5c42; font-size: 12px; white-space: nowrap; }
.wa-glory { display: inline-flex; align-items: center; gap: 4px; font-weight: 800; font-size: 13px; }
.wa-glory.up { color: #3d6127; }
.wa-glory.down { color: #8c1616; }
.wa-glory .chip-ico { width: 16px; height: 16px; }

@media (max-width: 640px) {
  .wa-face { width: 72px; height: 72px; flex-basis: 72px; }
  .wa-raid { flex-wrap: wrap; }
  .wa-raid-side { flex-direction: row; width: 100%; justify-content: space-between; }
}

/* Aug 11 2026 — the battle icon batch. Twenty-three renders took over from the
   emoji that were doing real work in a fight: the Chronicle's leading marks,
   the Divine Favor buttons, the Flee banner. */
.log-ico { width: 20px; height: 20px; vertical-align: -5px; margin-right: 2px; }
.btn-ico { width: 20px; height: 20px; vertical-align: -5px; }
/* A spent favour used to say "✓". It says so. */
.fav-spent {
  margin-left: 6px; font-size: 10px; letter-spacing: .06em; text-transform: uppercase;
  opacity: .75;
}

/* Aug 11 2026 — the Agora is the trade gate now: no market square, no
   caravans in either direction. When a member has not built one the hall
   says so where the form used to be, and points at the fix. */
.k-need-agora {
  display: flex; gap: 12px; align-items: flex-start;
  padding: 12px 14px; margin-bottom: 12px; border-radius: 6px;
  background: rgba(186, 26, 26, .08);
  border: 1px solid rgba(140, 22, 22, .35);
  border-left: 4px solid var(--error, #ba1a1a);
}
.k-need-agora-ico { width: 46px; height: 46px; flex: 0 0 46px; object-fit: contain; }
/* Aug 11 2026 — "remove here build one." No control in the notice: it is a
   requirement, printed once, on the panel whose form the missing building
   takes away. Two lines, so it reads as a footnote rather than an alarm. */
.k-need-agora { align-items: center; padding: 10px 12px; }
.k-need-agora b { font-size: 15px; }
.k-call-row { display: flex; gap: 8px; margin-bottom: 12px; flex-wrap: wrap; align-items: center; }
/* the user's two carved "Request" / "Donate" plaques */
.btn-plaque-request { background-image: url(/assets/ui/btn_request.png?v=1); width: 132px; height: 44px; }
.btn-plaque-donate { background-image: url(/assets/ui/btn_donate.png?v=1); width: 112px; height: 38px; }
/* section headings carry their icon at heading size */
.h-ico { width: 24px; height: 24px; vertical-align: -5px; margin-right: 4px; }

/* ─────────────────────────────────────────────────────────────────────────
   Aug 11 2026 — Koinon War, rebuilt round three questions: am I winning,
   what do I do now, and what is this place. See the note at the top of
   views/war.js.
   ───────────────────────────────────────────────────────────────────────── */

/* 1 · the score, as one bar instead of two numbers */
.war-score-row { display: flex; align-items: center; gap: 20px; flex-wrap: wrap; }
.war-side { text-align: center; min-width: 130px; }
.war-side .label-caps { color: #6d5c42; }
.war-pts { font-size: 2.1rem; font-weight: 800; line-height: 1.05; }
.war-side.mine .war-pts { color: var(--secondary); }
.war-side.theirs .war-pts { color: var(--error); }
.war-rate { font-size: 12px; font-weight: 700; opacity: .8; }
.war-bar-wrap { flex: 1; min-width: 240px; }
.war-bar {
  height: 16px; border-radius: 8px; overflow: hidden;
  background: rgba(186, 26, 26, .55);
  border: 1px solid var(--outline);
  box-shadow: inset 0 2px 5px rgba(0, 0, 0, .3);
}
.war-bar-mine {
  height: 100%;
  background: linear-gradient(180deg, #3d8fc4, #1c6390);
  box-shadow: 2px 0 6px rgba(0, 0, 0, .35);
  transition: width .5s ease;
}
.war-verdict { text-align: center; margin-top: 8px; font-size: 15px; }
.war-forecast { text-align: center; font-size: 13px; }
/* Aug 11 2026 — "Put Score bar and PREPARATION in scroll." Both panels moved
   onto the parchment 9-slice the Koinon hall uses; these are the adjustments
   that go with living on paper rather than on a dark slab. */
.war-scroll { max-width: 1120px; }
.war-scroll .war-score, .war-scroll .war-orders {
  background: none; border: none; box-shadow: none; padding: 0;
}
.war-scroll .war-orders { display: flex; gap: 14px; align-items: center; margin-top: 6px;
  padding-top: 14px; border-top: 1px solid rgba(122, 90, 58, .3); }
.war-scroll .war-side .label-caps, .war-scroll .war-orders-title { color: #7a520f; opacity: 1; }
.war-scroll .war-pts { color: #2a2013; }
.war-scroll .war-verdict, .war-scroll .war-orders-line { color: #2a2013; }
.war-scroll .war-forecast, .war-scroll .war-points,
.war-scroll .war-orders-levy, .war-scroll .war-orders-clock { color: #6d5a3e; }
.war-scroll .war-bar { background: rgba(90, 66, 34, .22); }
/* the running points total, demoted under the buildings score it used to be */
.war-points { text-align: center; font-size: 12px; margin-top: 6px; }
.war-points b { color: #5c430e; }
/* Aug 11 2026 — "The price for winning the war - is weekly fee that was paid by
   another kainon through the week." A war with a real stake has to say what it
   is, at the top, next to the score it is riding on. */
.war-stake {
  display: flex; align-items: center; gap: 14px;
  margin-top: 16px; padding-top: 14px;
  border-top: 1px solid rgba(122, 90, 58, .3);
  background: none; border-radius: 0;
  font-size: 13px; line-height: 1.55;
}
.war-stake-ico { width: 40px; height: 40px; flex: 0 0 40px; object-fit: contain; }
.war-stake .label-caps { display: block; }
.war-stake b { font-size: 15px; }
.war-stake .muted { display: block; }

/* 2 · one line of orders and one button */
.war-orders { display: flex; align-items: center; gap: 16px; flex-wrap: wrap; }
.war-orders-ico { width: 54px; height: 54px; flex: 0 0 54px; object-fit: contain; }
/* Aug 11 2026 — the gold and red rules down the left of this panel went with
   the rest of the side bars. On the scroll they were a stripe of UI paint
   across a sheet of parchment, and the panel already says which state it is in
   with the words "Preparation" or "Every 5 hours the board fights". */
.war-orders.is-prep, .war-orders.is-live { border-left: none; }
.war-orders-title { color: #6d4d18; }
.war-orders-line { margin: 2px 0 4px; }
/* Aug 11 2026 — the weekly levy. The board fills up on its own between visits,
   which is a good surprise only if it was announced first. */
.war-orders-levy { font-size: 13px; font-style: italic; margin: 0 0 4px; opacity: .85; }
.war-orders-clock { font-size: 13px; }
.war-orders-act { text-align: right; }
.war-orders-power { font-size: 12px; margin-top: 6px; }

/* the front — the map keeps its place, the icons are real art now */
.war-map { min-height: 380px; margin-bottom: 18px; }
.war-node .disc { overflow: hidden; padding: 8px; }
.war-node .disc img { width: 100%; height: 100%; object-fit: contain; }
.war-node .tag-name { font-weight: 700; }
.war-node .tag-own { font-size: 10px; letter-spacing: .06em; }
.war-node.mine .tag-own { background: rgba(28, 99, 144, .9); }
.war-node.theirs .tag-own { background: rgba(147, 0, 10, .9); }
.war-node.neutral .tag-own { background: rgba(85, 67, 54, .85); }

/* 3 · one card per place: what it pays, who has it, what holds it */
.war-nodes { display: flex; flex-direction: column; gap: 10px; }
.war-node-card {
  display: flex; gap: 14px; align-items: center;
  padding: 12px 14px; border-radius: 8px;
  background: var(--surface-low, rgba(255, 251, 240, .55));
  border: 1px solid var(--outline); border-left-width: 5px;
  transition: box-shadow .3s;
}
.war-node-card.mine { border-left-color: var(--secondary); }
.war-node-card.theirs { border-left-color: var(--error); }
.war-node-card.neutral { border-left-color: #8a7350; }
.war-node-card.flash { box-shadow: 0 0 0 3px rgba(255, 214, 120, .9); }
.wn-ico { width: 56px; height: 56px; flex: 0 0 56px; object-fit: contain; }
.wn-main { flex: 1; min-width: 0; }
.wn-head { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; }
.wn-yield { font-weight: 800; color: #5c430e; }
.wn-yield .muted { font-weight: 400; font-size: 12px; }
/* .wn-blurb is no longer rendered (Aug 11 2026 — "remove all text under the
   building name"); the rule stays because NODE_ART still carries the text. */
.wn-blurb { font-size: 13px; margin: 2px 0 4px; }
.wn-hold { font-size: 13px; display: flex; gap: 6px; flex-wrap: wrap; align-items: baseline; }
.wn-owner { font-weight: 700; }
.wn-owner.mine { color: var(--secondary); }
.wn-owner.theirs { color: var(--error); }
.wn-owner.neutral { color: #6d5c42; }
.wn-def { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin-top: 6px; }
.wn-def .label-caps { color: #6d5c42; }
.wn-unit { display: inline-flex; align-items: center; gap: 3px; font-weight: 700; font-size: 13px; }
.wn-unit img { width: 24px; height: 24px; object-fit: contain; }
.wn-wall { font-size: 12px; font-style: italic; opacity: .8; }
.wn-act { flex: 0 0 150px; text-align: center; }
.wn-odds { font-size: 12px; font-weight: 800; letter-spacing: .04em; text-transform: uppercase; }
.wn-odds-desperate { color: #8c1616; }
.wn-odds-uphill { color: #a0621a; }
.wn-odds-even { color: #6d5c42; }
.wn-odds-favoured, .wn-odds-sure { color: #3d6127; }
.wn-odds-bar {
  height: 6px; border-radius: 3px; margin: 4px 0 8px;
  background: rgba(90, 66, 34, .2); overflow: hidden;
}
.wn-odds-bar i { display: block; height: 100%; background: linear-gradient(90deg, #b05050, #d4a017 55%, #5c8a3a); }
.wn-yours { font-size: 12px; font-weight: 700; color: var(--secondary); }
.war-done-ico { width: 88px; height: 88px; object-fit: contain; }
.war-footnote { margin-top: 16px; font-size: 13px; line-height: 1.6; }
.wn-rew { display: inline-flex; align-items: center; gap: 3px; font-weight: 700; }
.wn-rew img { width: 18px; height: 18px; }

@media (max-width: 760px) {
  .war-node-card { flex-wrap: wrap; }
  .wn-act { flex: 1 0 100%; display: flex; align-items: center; gap: 10px; }
  .wn-odds-bar { flex: 1; margin: 0; }
}

/* ─────────────────────────────────────────────────────────────────────────
   Aug 11 2026 — "Make progress window on wooden board with scroll on the
   left for side menu."

   The window is the carved board (frame_wood.png is exactly that: planks
   inside a bronze-and-verdigris meander, 978×959 with 118/116 pilasters and
   68/70 caps), and the left pane hangs as a scroll (scroll_reserve.png,
   878×889 — rod 0–76 at the top, 815–889 at the foot, torn parchment edges
   about 60px in on each side).

   The scroll is sliced WITHOUT `fill`: its centre carries a painted laurel
   which, stretched down a tall menu, smears. The parchment colour is laid on
   underneath instead, so only the rods and torn edges come from the art.
   ───────────────────────────────────────────────────────────────────────── */
.progress-fullscreen {
  background: rgba(14, 11, 7, .95);
  padding: clamp(10px, 2vh, 24px) clamp(10px, 2vw, 34px) clamp(10px, 2vh, 26px);
}
.progress-fullscreen-header { padding: 2px 4px 12px; }
/* Aug 11 2026 — the frame goes round the BUILDINGS and nothing else. The
   title sits above it on the scrim, the scroll hangs free beside it, and the
   wood is the ground the tree stands on: the light sheet that used to be laid
   over the planks was a window inside a window. */
/* Aug 11 2026 — "make so frames are not stretched, but normal size and fit
   everything, same with scroll."
   
   The slices were being squeezed into border widths that had nothing to do
   with their own proportions: the pilasters are 118px and 116px of the source
   and were drawn into 36px, while the caps are 68px and 70px drawn into 34px.
   Horizontal art at 31% and vertical at 50% is a frame with its uprights
   crushed, which is exactly what it looked like. Every border-width is now the
   slice times ONE scale factor (0.45 for the board, 0.7 for the scroll), so
   the carving keeps its shape; and the repeat is `round` rather than `stretch`,
   so the meander runs as whole repeats along an edge of any length instead of
   being smeared to fit. */
.progress-board {
  flex: 1; min-width: 0; min-height: 0; display: flex; flex-direction: column;
  border-style: solid; border-color: transparent;
  border-width: 31px 52px 32px 53px;
  border-image-source: url('/assets/ui/frame_wood.png?v=47');
  border-image-slice: 68 116 70 118;
  border-image-width: 31px 52px 32px 53px;
  border-image-repeat: round round;
  /* The planks come from their OWN texture rather than the frame's fill slice:
     `round` tiles the fill as well as the edges, and a tiled centre put hard
     seams across the middle of the board at every repeat. One cropped panel,
     scaled to cover, is a single piece of wood. */
  background: url('/assets/ui/wood_panel.jpg?v=47') center / cover no-repeat #3d2a19;
  background-clip: padding-box;
  filter: drop-shadow(0 14px 34px rgba(0, 0, 0, .55));
}
.progress-board .progress-tree-scroll {
  background: none; padding: 8px; margin: 0;
}
.progress-fullscreen-header h2 { color: #f6ecd2; text-shadow: 0 2px 5px rgba(0, 0, 0, .8); }
.progress-fullscreen-header .muted { color: #cbb691; }
.progress-fullscreen-header .close { padding: 0; }
.progress-fullscreen-header .close img { width: 30px; height: 30px; }
.progress-body { border-top: none; gap: 14px; }

/* the hanging scroll that carries the side menu.
   Aug 11 2026 — "take in consideration spacing". The rods are painted into
   the border slices, so the parchment's usable area is what is INSIDE them;
   content laid against that edge sat under the roll. The pane keeps its own
   breathing room now, and the scroll hangs from the top of the row rather
   than stretching to whatever height the tree happens to be. */
.progress-scroll-menu {
  /* Hangs from the top and stops where its parchment ends, rather than being
     stretched to whatever height the tree happens to be — a scroll with a
     foot of blank vellum under a four-line card reads as a layout accident. */
  flex: none; width: 400px; min-height: 0; align-self: flex-start; max-height: 100%;
  margin: 0;
  border-style: solid; border-color: transparent;
  border-width: 70px 42px 66px 42px;
  border-image-source: url('/assets/ui/scroll_reserve.png?v=47');
  border-image-slice: 100 60 95 60;
  border-image-width: 70px 42px 66px 42px;
  border-image-repeat: round round;
  background: #ece0c6;
  background-clip: padding-box;
  display: flex; flex-direction: column;
  filter: drop-shadow(0 10px 26px rgba(0, 0, 0, .45));
}
.psm-inner {
  flex: 1; min-height: 0; overflow-y: auto;
  padding: 4px 14px 10px; color: #2a2013;
  scrollbar-width: thin; scrollbar-color: rgba(90, 66, 34, .45) transparent;
}
.psm-inner::-webkit-scrollbar { width: 8px; }
.psm-inner::-webkit-scrollbar-thumb { background: rgba(90, 66, 34, .4); border-radius: 4px; }
.psm-inner .muted { color: #6d5c42; }

/* two leaves of the same scroll */
.psm-tabs { display: flex; gap: 6px; margin: 0 0 10px; }
.psm-tab {
  flex: 1; padding: 7px 6px; cursor: pointer;
  font-size: 12px; font-weight: 800; letter-spacing: .05em; text-transform: uppercase;
  color: #6d5c42; background: rgba(90, 66, 34, .1);
  border: 1px solid rgba(90, 66, 34, .35); border-radius: 4px;
}
.psm-tab.is-on { background: rgba(212, 160, 23, .3); border-color: var(--gold, #d4a017); color: #3d2c10; }
.psm-head {
  color: #6d4d18; margin: 10px 0 6px; font-size: 11px; letter-spacing: .09em;
  border-bottom: 1px solid rgba(90, 66, 34, .28); padding-bottom: 3px;
}
.psm-queue-row { border-color: rgba(90, 66, 34, .25); color: #2a2013; }

/* the stage index — one row per stage, its own progress, click to jump */
.psm-stages { display: flex; flex-direction: column; gap: 4px; }
.psm-stage {
  display: flex; align-items: center; gap: 8px; width: 100%;
  padding: 5px 7px; cursor: pointer; text-align: left;
  background: rgba(90, 66, 34, .07);
  border: 1px solid rgba(90, 66, 34, .25); border-radius: 4px;
  color: #2a2013;
}
.psm-stage:hover { background: rgba(212, 160, 23, .22); }
.psm-stage.has-ready { border-color: var(--gold, #d4a017); }
.psm-stage.is-done { opacity: .6; }
.psm-stage-n {
  flex: 0 0 24px; height: 24px; border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  font-size: 12px; font-weight: 800;
  background: rgba(90, 66, 34, .25); color: #2a2013;
}
.psm-stage.has-ready .psm-stage-n { background: var(--gold, #d4a017); color: #3d2c10; }
.psm-stage-main { flex: 1; min-width: 0; }
.psm-stage-bar {
  display: block; height: 5px; border-radius: 3px; overflow: hidden;
  background: rgba(90, 66, 34, .2);
}
.psm-stage-bar i { display: block; height: 100%; background: linear-gradient(90deg, #b8912f, #5c8a3a); }
.psm-stage-txt { display: block; font-size: 11px; color: #6d5c42; margin-top: 2px; }
.psm-stage-txt b { color: #5c430e; }

/* the stage number in the board's left margin lights up when jumped to */
.pg-stage-label.is-lit {
  color: #5c430e; font-weight: 800;
  background: rgba(212, 160, 23, .28); border-radius: 4px;
}

/* Narrower viewports scale the whole frame down together — one factor, so the
   carving never distorts, only shrinks. */
@media (max-width: 1200px) {
  .progress-scroll-menu { width: 340px; border-width: 60px 36px 57px 36px; border-image-width: 60px 36px 57px 36px; }
  .progress-board { border-width: 27px 46px 28px 47px; border-image-width: 27px 46px 28px 47px; }
}
@media (max-width: 900px) {
  .progress-body { gap: 8px; }
  .progress-scroll-menu { width: 280px; border-width: 50px 30px 48px 30px; border-image-width: 50px 30px 48px 30px; }
  .progress-board { border-width: 20px 35px 21px 35px; border-image-width: 20px 35px 21px 35px; }
}
.k-need-agora .grow { min-width: 0; }
.k-need-agora-line { font-style: italic; margin-bottom: 8px; }
/* the closing note sits on the war backdrop, so it needs its own ground */
/* Aug 11 2026 — "put in scroll: Orders are written any time…". It sits on the
   parchment now like everything else that explains itself, so the slab it used
   to carry goes. */
.k-scroll.war-rules { max-width: 900px; margin: 26px auto 0; }
.war-rules .war-footnote {
  background: none; border: none; border-radius: 0; padding: 0;
  color: #4a3a24; text-align: center; font-size: 13.5px; line-height: 1.7;
}
.war-rules .wn-rew { color: #5c430e; }

/* ─────────────────────────────────────────────────────────────────────────
   Aug 11 2026 — the Progress audit. The detail card is carved marble like
   every other card in the game (unit_card_frame.png, the same 9-slice the
   Army page uses), each fact sits on its own bronze-clasped plate
   (plate_sm.png), and the status pill carries real art instead of an emoji.
   ───────────────────────────────────────────────────────────────────────── */
/* A full marble card frame inside a parchment scroll is frame fighting frame,
   so the card keeps the scroll's own ground and takes a slim carved edge —
   the FACTS are where the real plates go, below. */
.progress-detail {
  display: block; color: #2a2013;
  background: rgba(255, 253, 247, .75);
  border: 1px solid rgba(90, 66, 34, .35);
  border-radius: 4px; padding: 12px 12px 14px;
  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, .5), 0 2px 8px rgba(90, 66, 34, .18);
}
.pd-head { display: flex; align-items: center; gap: 10px; margin: -8px 0 8px; }
.pd-head-text { min-width: 0; }
.progress-detail .pd-photo-wrap { flex: none; width: 64px; height: 64px; }
.progress-detail .pd-name { font-weight: 800; font-size: 15px; color: #2a2013; line-height: 1.2; }
/* the one place a clasped plate is the right shape */
.progress-detail .pd-level-badge {
  display: inline-block; margin: 5px 0 0; padding: 2px 12px; min-height: 26px;
  line-height: 22px; text-align: center;
  font-size: 11px; font-weight: 800; letter-spacing: .05em; text-transform: uppercase;
  color: #4a3413;
  border-style: solid; border-color: transparent;
  border-width: 5px 22px 5px 22px;
  border-image: url(/assets/ui/plate_sm.png?v=45) 42 78 42 78 fill / 5px 22px 5px 22px stretch;
}
.progress-detail .pd-desc { font-size: 12px; color: #6d5c42; margin: 0 0 8px; }

/* one fact, one plate */
/* The fact rows are carved grooves in the parchment, not plates: plate_sm's
   bronze end-caps are ~70px of a 399px image, so at row height they swallow
   the label whole. The plate goes where its shape actually fits — on the
   level badge, which is three words long. */
.progress-detail .pd-row {
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
  flex-wrap: wrap;
  font-size: 12.5px; margin-bottom: 6px; min-height: 30px; padding: 6px 10px;
  background: linear-gradient(180deg, rgba(90, 66, 34, .07), rgba(90, 66, 34, .12));
  border: 1px solid rgba(90, 66, 34, .3);
  border-left: 3px solid rgba(184, 145, 47, .8);
  border-radius: 3px;
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, .45);
  color: #2a2013;
}
.progress-detail .pd-label {
  color: #6d4d18; font-size: 10.5px; font-weight: 800;
  letter-spacing: .06em; text-transform: uppercase; white-space: nowrap;
}
.progress-detail .pd-val { font-weight: 700; text-align: right; flex: 1 1 auto; min-width: 0; }
.progress-detail .pd-val.is-warn { color: #8c1616; }

.progress-detail .pd-status {
  display: flex; align-items: center; gap: 8px;
  font-size: 12.5px; font-weight: 700; margin-top: 10px; padding-top: 9px;
  border-top: 1px solid rgba(90, 66, 34, .3);
}
.pd-status-ico { width: 24px; height: 24px; object-fit: contain; flex: none; }
/* a status with no art gets a carved bullet in its own colour */
.progress-detail .pd-status:not(:has(.pd-status-ico))::before {
  content: ''; flex: none; width: 10px; height: 10px; border-radius: 50%;
  background: currentColor; box-shadow: 0 0 0 2px rgba(255, 255, 255, .6);
}
.progress-detail .pd-status-ready { color: #3d6127; }
.progress-detail .pd-status-locked { color: #8c1616; }
.progress-detail .pd-status-building { color: #2f6fb0; }
.progress-detail .pd-status-maxed { color: #5c430e; }
.progress-detail .pd-chain { border-top-color: rgba(90, 66, 34, .3); }
.progress-detail .pd-chain-label { color: #6d4d18; }
.progress-detail .pd-chain-list { color: #2a2013; }

/* the queue rows in the scroll are cut the same way as the fact rows */
.psm-queue-row {
  background: linear-gradient(180deg, rgba(90, 66, 34, .07), rgba(90, 66, 34, .12));
  border: 1px solid rgba(90, 66, 34, .3);
  border-left: 3px solid rgba(47, 111, 176, .8);
  border-radius: 3px;
  padding: 6px 10px; min-height: 30px; margin-bottom: 5px;
  color: #2a2013;
}
.psm-queue-row .muted { color: #6d5c42; }

/* the board's own node overlays — a drawn padlock, and real art for the rest */
.pgl-lock {
  position: absolute; right: 2px; bottom: 2px; width: 13px; height: 11px;
  border-radius: 2px; background: #6d5c42; box-shadow: 0 1px 2px rgba(0, 0, 0, .5);
}
.pgl-lock::before {
  content: ''; position: absolute; left: 3px; top: -5px;
  width: 7px; height: 6px; border: 2px solid #6d5c42;
  border-bottom: none; border-radius: 5px 5px 0 0;
}
img.pgl-building, img.pgl-star {
  position: absolute; right: 0; bottom: 0; width: 18px; height: 18px;
  object-fit: contain; filter: drop-shadow(0 1px 2px rgba(0, 0, 0, .55));
}

/* ─────────────────────────────────────────────────────────────────────────
   Aug 11 2026 — the muster. Orders on the card, the enemy's half of the
   board deliberately dark. See the note at the top of views/war.js.
   ───────────────────────────────────────────────────────────────────────── */
.war-orders-act { text-align: right; display: flex; flex-direction: column; gap: 4px; }
.war-tally { display: flex; align-items: baseline; justify-content: flex-end; gap: 8px; font-size: 13px; }
.war-tally .label-caps { color: #6d5c42; font-size: 10px; }

/* how many of your Koinon's orders point at this place, on the map disc */
.war-node-pledge {
  position: absolute; top: -4px; right: -4px;
  min-width: 22px; height: 22px; padding: 0 6px;
  display: inline-flex; align-items: center; justify-content: center;
  border-radius: 11px; background: var(--secondary, #1c6390);
  border: 2px solid rgba(255, 245, 225, .9);
  color: #fff; font-size: 12px; font-weight: 800; line-height: 1;
  box-shadow: 0 2px 6px rgba(0, 0, 0, .5);
}

.wn-yield .muted { font-weight: 400; font-size: 12px; }
.wn-act { flex: 0 0 190px; text-align: center; display: flex; flex-direction: column; gap: 8px; align-items: stretch; }
.wn-committed {
  padding: 6px 8px; border-radius: 4px;
  background: rgba(90, 66, 34, .1); border: 1px solid rgba(90, 66, 34, .28);
}
.wn-committed .label-caps { display: block; font-size: 10px; color: #6d5c42; }
.wn-committed b { font-size: 14px; }

/* The orders themselves — one row per member's pledge. Aug 11 2026: boxes and
   coloured left rules out (see .wn-force above), ruled lines in. A pledge list
   is a list; it does not need a frame around every entry. Your own row still
   stands out, but by a wash of gold across the line rather than by a bar. */
.wn-orders { display: flex; flex-direction: column; margin-top: 10px; }
.wn-order {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  padding: 7px 4px; font-size: 13px;
  background: none; border: none;
  border-bottom: 1px solid rgba(122, 90, 58, .2);
}
.wn-order:last-child { border-bottom: none; }
.wn-order.is-mine {
  background: linear-gradient(90deg, rgba(212, 160, 23, .22), rgba(212, 160, 23, 0) 78%);
}
.wn-order-kind {
  font-size: 10px; font-weight: 800; letter-spacing: .06em; text-transform: uppercase; color: #6d5c42;
  min-width: 54px;
}
.wn-order.defend .wn-order-kind { color: #1c6390; }
.wn-order.attack .wn-order-kind { color: #8c1616; }
.wn-order-men { display: inline-flex; gap: 8px; flex-wrap: wrap; margin-left: auto; }
.wn-recall {
  border: 1px solid rgba(140, 22, 22, .5); background: rgba(186, 26, 26, .1);
  color: #8c1616; border-radius: 3px; padding: 2px 8px;
  font-size: 11px; font-weight: 700; cursor: pointer;
}
.wn-recall:hover { background: rgba(186, 26, 26, .2); }
.wn-noorders { font-size: 12.5px; margin-top: 8px; font-style: italic; }
/* the deliberate blank where the enemy's strength would be */
.wn-fog {
  margin-top: 8px; padding: 0; font-size: 12.5px; font-style: italic;
  background: none; border: none; color: #6d5a3e;
}
.wn-story { font-size: 12px; margin-top: 6px; }
.wn-story .ok { color: #3d6127; }
.wn-story .bad { color: #8c1616; }

.war-send { min-width: min(560px, 84vw); }
.war-send .row.sel { background: rgba(212, 160, 23, .18); }
.war-send-foot { display: flex; align-items: center; gap: 12px; margin-top: 14px; }

@media (max-width: 760px) {
  .wn-act { flex: 1 0 100%; flex-direction: row; }
  .wn-committed { flex: 1; text-align: left; }
}

/* Aug 11 2026 — "I see that Divine Favors icons are still not at place":
   the battle view had them, but the panel a player actually equips from —
   the Army page's Divine Favors list — was still drawing the emoji, and so
   were the temple card and the army picker's "equipped" line. */
.fav-medal { width: 44px; height: 44px; object-fit: contain; flex: none; }
.fav-medal.sm { width: 28px; height: 28px; margin-right: 6px; }
.fav-locked .fav-medal { filter: grayscale(55%) brightness(.95); opacity: .8; }
.fav-inline { vertical-align: -4px; }

/* ─────────────────────────────────────────────────────────────────────────
   Aug 11 2026 — the front, cut down the middle. Left is what you hold and
   can lose, right is what you do not and could win; a place crosses over
   when it changes hands. The map that used to sit above this is gone — it
   was decoration pretending to be information.
   ───────────────────────────────────────────────────────────────────────── */
/* Aug 11 2026 — "Use already existing frames styles and make it UI/UX
   friendly." The board the two columns sit on is the Progress window's carved
   wooden frame, reused at exactly the 9-slice scale that was measured for it
   (uniform k per side, so nothing stretches) with wood_panel.jpg as the
   interior. One board, two sides — which is what a war is. */
.war-board {
  border: solid transparent;
  border-width: 31px 52px 32px 53px;
  border-image-source: url('/assets/ui/frame_wood.png?v=47');
  border-image-slice: 68 116 70 118;
  border-image-width: 31px 52px 32px 53px;
  border-image-repeat: round round;
  background: url('/assets/ui/wood_panel.jpg?v=47') center / cover no-repeat #3d2a19;
}
@media (max-width: 720px) {
  .war-board {
    border-width: 18px 30px 19px 31px;
    border-image-width: 18px 30px 19px 31px;
  }
}
.war-fronts {
  display: grid; grid-template-columns: 1fr 1fr; gap: 18px; align-items: start;
}
/* Aug 11 2026 — the column's own pale sheet went too. Every card is a piece of
   paper now, and they were sitting on a second, bigger, paler piece of paper,
   which is what made the whole board look washed out: two backgrounds arguing
   under one set of numbers. The cards sit on the wood, and the coloured header
   is all the column needs to say whose side it is. */
.war-col {
  background: none;
  border: none; border-radius: 10px;
  overflow: hidden;
}
.war-col-head {
  display: flex; align-items: center; gap: 12px;
  padding: 12px 16px; color: #f6ecd2;
  border-bottom: 1px solid rgba(0, 0, 0, .25);
}
.war-col-head.ours { background: linear-gradient(180deg, #2b7bab, #1c6390); }
.war-col-head.theirs { background: linear-gradient(180deg, #a83b3b, #8c1616); }
.war-col-ico {
  width: 40px; height: 40px; object-fit: contain; flex: none;
  filter: drop-shadow(0 2px 4px rgba(0, 0, 0, .6));
}
.war-col-title { font-size: 1.05rem; font-weight: 800; letter-spacing: .01em; }
.war-col-sub { font-size: 12px; opacity: .88; }
.war-col-count {
  margin-left: auto; font-size: 1.35rem; font-weight: 800; white-space: nowrap;
  text-shadow: 0 2px 4px rgba(0, 0, 0, .5);
}
.war-col-count i { font-size: 11px; font-style: normal; opacity: .8; }
.war-col-body { display: flex; flex-direction: column; gap: 10px; padding: 12px; }
.war-col-empty {
  display: flex; gap: 12px; align-items: center; padding: 18px 14px;
  font-size: 13px; line-height: 1.5;
}
.war-col-empty img { width: 64px; height: 64px; object-fit: contain; opacity: .7; flex: none; }

/* One card per building. Aug 11 2026 — "Put all buildings with the things in
   paper frames": each card is a torn sheet of papyrus with the Greek-key band
   along its top and bottom edge (frame_paper_wide.png, cut from the supplied
   art), 9-sliced at a uniform k=0.36 of the source slice so no side stretches.
   The `fill` slice is deliberately NOT used — the sheet's middle is a soft wash
   that tiles with visible seams — so the interior is a separate cover-scaled
   crop of the same paper. Which side owns the building is a coloured rule down
   the left edge, over the paper, since the frame itself has no side to tint. */
.war-fronts .war-node-card {
  display: flex; gap: 18px; align-items: flex-start; flex-wrap: wrap;
  position: relative;
  border: solid transparent;
  border-width: 30px 22px 42px 22px;
  border-image-source: url('/assets/ui/frame_paper_wide.png?v=48');
  border-image-width: 30px 22px 42px 22px;
  /* `round` across, `stretch` down. The two axes want opposite things: the
     Greek-key band runs horizontally and must tile at whole repeats or the
     pattern smears, while everything vertical — the torn side edges and the
     sheet's own wash — is irregular to begin with and stretches invisibly. Set
     both to `round` and a card taller than the middle slice tiles it, which
     draws a hairline straight across the paper where the two copies meet. */
  border-image-repeat: round stretch;
  /* Aug 11 2026 — "Can you use big paper background."
     Yes, and it is the right fix. The interior used to be a SEPARATE image —
     a crop of the sheet's middle, scaled to the card behind the frame — and a
     separate image can never line up: the middle of this papyrus is its
     lightest part and the edges its darkest, so however the crop was chosen
     there was a visible step where the two met, a pale panel inset inside a
     darker border.
     `fill` takes the middle out of the SAME nine-slice as the edges, so the
     wash, the stains and the watermarks run continuously from one to the
     other. One sheet, at whatever size the card happens to be. The middle
     region is bigger than any card, so `round` scales it once rather than
     tiling it — which is what stops the seams that made this approach look
     wrong the first time it was tried. */
  border-image-slice: 79 40 110 40 fill;
  /* Aug 11 2026 — "let's remove pale background behind it." The flat cream was
     a fallback tone for the instant before the sheet decodes, but the sheet's
     torn edge is anti-aliased: a rectangle of solid cream sat behind an
     irregular tear and showed through it as a pale fringe, squaring off the one
     edge whose whole point is that it is ragged. The border-image's own fill is
     the surface; there is nothing for a fallback to do but spoil it. */
  background: none;
  /* "Work on the spacing in Wars. as you can see the text is close to border."
     (Aug 11 2026, second pass.)
     The border-width is what the frame is DRAWN at — 14px down each side — but
     the torn edge painted into that strip wanders further in than that, and the
     Greek-key band at top and bottom is opaque right to its inner line. Twenty
     pixels of padding put the first letter of every line about six pixels clear
     of ragged paper, which reads as touching it. This is a proper margin: the
     text now sits where a scribe would have started writing. */
  padding: 22px 36px 18px;
  transition: box-shadow .3s;
}
.war-col-body { gap: 16px; }
/* The owner's colour used to be a rule down the left edge of every card. It
   went with the other side bars (Aug 11 2026) — which column a card is in
   already says whose it is, twice over, and the header above the column says
   it a third time in the same colour. */
@media (max-width: 720px) {
  .war-fronts .war-node-card {
    border-width: 20px 10px 28px 10px;
    border-image-width: 20px 10px 28px 10px;
  }
}
/* Aug 11 2026 — the "8/day" plaque is gone from under the sprite ("remove
   number/day in kainon under the building"), so the portrait is just the
   building, sized up a little now that nothing hangs off it. */
.wn-portrait { position: relative; flex: none; width: 84px; text-align: center; }
.wn-portrait .wn-ico { width: 76px; height: 76px; object-fit: contain; display: block; margin: 0 auto; }
.war-fronts .wn-main { flex: 1 1 220px; min-width: 0; }
.war-fronts .wn-head {
  display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap;
  min-height: 34px; align-content: center;
}
.war-fronts .wn-head b { font-size: 15px; }
.war-fronts .wn-owner { font-size: 11px; font-weight: 800; letter-spacing: .05em; text-transform: uppercase; }
.war-fronts .wn-owner.ours { color: var(--secondary, #1c6390); }
.war-fronts .wn-owner.theirs { color: var(--error, #ba1a1a); }
/* Aug 11 2026 — "Work on the spacing in Wars." The action row used to carry a
   rule above it and twenty-six pixels of clearance; now that the tally sits on
   its own carved plank, both were separating things that were already
   separate, and left a hand's width of blank paper on every card. */
.war-fronts .wn-act {
  flex: 1 1 100%; display: flex; flex-direction: row; align-items: center;
  justify-content: space-between; gap: 12px; text-align: left;
  margin-top: 10px; padding-top: 0; border-top: none;
}
/* Aug 11 2026 — "Put KOINON SENDS 48 men none of them yours in wooden frames."
   The same carved plank the section titles use (plank_title.png), 9-sliced at
   two-thirds its scale: the meander bands repeat rather than stretch, because a
   meander pulled to double width stops being a meander. It gives the one line
   on the card that is a RESULT — what the hall is actually sending — something
   to sit on, against the paper everything else is written on. */
.war-fronts .wn-committed {
  flex: 0 1 auto; display: flex; align-items: baseline; gap: 9px;
  border-style: solid; border-color: transparent;
  border-width: 14px 18px 12px 18px;
  border-image: url('/assets/ui/plank_title.png?v=32') 78 30 68 30 fill / 14px 18px 12px 18px round stretch;
  /* the plank's own fill slice is the surface — the base rule's tint would sit
     under it doing nothing but confusing anyone reading the computed style */
  background: none;
  padding: 0 10px; min-height: 30px; border-radius: 0;
  color: #f6ecd2; text-shadow: 0 1px 3px rgba(0, 0, 0, .75);
}
.war-fronts .wn-committed .label-caps { color: #d9c79c; }
.war-fronts .wn-committed b { color: #fff3d4; }
.war-fronts .wn-committed .wn-committed-mine { color: #c6b393; }
@media (max-width: 720px) {
  .war-fronts .wn-committed {
    border-width: 11px 14px 10px 14px;
    border-image-width: 11px 14px 10px 14px;
  }
}
.war-fronts .wn-committed .label-caps { display: inline; }
/* the hall's tally, and your own share of it — one line, quietly */
.wn-committed-mine { font-size: 11px; font-style: italic; }
.war-fronts .wn-send { flex: none; min-width: 116px; }

@media (max-width: 980px) {
  .war-fronts { grid-template-columns: 1fr; }
}

/* One line of muster arithmetic per card: what will stand on your ground, and
   what your Koinon is sending at theirs.
   Aug 11 2026 — "remove pale background and side bars (yellow, blue and red)."
   Every block on the card was a tinted box with a coloured rule down its left
   edge, which on a torn sheet of papyrus is a box drawn on a box: five nested
   rectangles per building, in four colours, before you reach a number. The
   tints and the rules are gone. What separates the blocks now is a hairline
   and honest space — the paper is the surface, and the colour that is left is
   the one place it carries meaning, in the numbers themselves. */
.wn-force {
  margin-top: 14px; padding: 0 0 0 2px;
  border: none; border-radius: 0; background: none;
}
.wn-force-head {
  display: flex; align-items: baseline; justify-content: space-between; gap: 10px;
  padding-bottom: 6px; border-bottom: 1px solid rgba(122, 90, 58, .28);
}
.wn-force-head .label-caps { color: #6d5c42; font-size: 10px; letter-spacing: .07em; }
.wn-force-head b { font-size: 16px; }
.wn-force.is-defence .wn-force-head b { color: #1c6390; }
.wn-force.is-assault .wn-force-head b { color: #8c1616; }
.wn-force-units { display: flex; flex-wrap: wrap; gap: 14px; margin-top: 9px; }
.wn-force-units .wn-unit { font-size: 13px; font-weight: 700; }
.wn-force-units .wn-unit img { width: 26px; height: 26px; }
/* .wn-force-foot and .wn-fog are no longer rendered (Aug 11 2026 — "remove as
   well text that goes after the troops"). */
.wn-force-foot { font-size: 11.5px; margin-top: 7px; line-height: 1.5; }
/* a row that has no control, and says why */
.k-needs-agora-tag {
  font-size: 11px; font-weight: 700; font-style: italic;
  color: #8c1616; white-space: nowrap;
}

/* ── the opening: what everything is made of ─────────────────────────────
   Aug 12 2026 — "put speaking of resources in the beggining and add icons when
   first introduced in how to play." */
.guide-opening { margin: 0 auto 34px; }
.guide-opening-sheet {
  background: rgba(253, 250, 242, .95);
  border: 1px solid rgba(120, 92, 48, .3);
  border-radius: 12px;
  box-shadow: 0 18px 46px rgba(20, 14, 6, .45);
  padding: 26px 38px 30px;
  width: min(1100px, 92vw); margin: 0 auto;
}
.guide-opening-sheet h2 { font-family: var(--serif); font-size: 1.5rem; color: #241a0e; margin: 0 0 8px; }
.guide-opening-sheet p { color: #4a3d2c; line-height: 1.7; margin: 0 0 14px; max-width: 76ch; }
.guide-res-grid {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(290px, 1fr));
  gap: 12px 22px; margin: 16px 0 18px;
}
.guide-res { display: flex; gap: 12px; align-items: flex-start; }
.guide-res-ico { width: 38px; height: 38px; object-fit: contain; flex: none; margin-top: 2px; }
.guide-res b { font-family: var(--serif); color: #241a0e; font-size: 1.02rem; }
.guide-res i { color: #8a7757; font-size: .8rem; font-style: normal; }
.guide-res span { display: block; color: #5a4a33; font-size: .9rem; line-height: 1.5; margin-top: 2px; }
.guide-opening-foot { border-top: 1px solid #e7dcc4; padding-top: 14px; margin-bottom: 0 !important; }
@media (max-width: 720px) { .guide-opening-sheet { padding: 20px 18px 24px; } }

/* A word marked with the thing it names, the first time a section says it. */
.guide-inline-ico { white-space: nowrap; }
.guide-inline-ico img {
  width: 1.05em; height: 1.05em; object-fit: contain;
  vertical-align: -.16em; margin-right: .22em;
}

/* ── hover, without the jitter ───────────────────────────────────────────
   Aug 12 2026 — "A lot of images glitch when you hover over them."

   Two causes, both mine. Animating `transform` on an element that also carries
   a `filter: drop-shadow(...)` makes the browser re-rasterise the shadow from
   the source alpha on every frame, which on a grid of forty medallions is a
   visible stutter. And transitioning `filter` itself — the saturate() on the
   nav plaques — repaints a border-image plaque each frame for the same reason.

   So: shadows that move are box-shadows (composited, not re-rastered), filters
   are set rather than transitioned, and anything that lifts on hover gets its
   own layer up front so the lift is a composite and not a repaint. */
.guide-med-ring,
.guide-rack-item,
.guide-four-tab,
.guide-step-tab {
  will-change: transform;
  backface-visibility: hidden;
  transform: translateZ(0);
}
.guide-med-ring {
  /* the drop-shadow moved into the box-shadow list above it */
  filter: none;
  box-shadow:
    0 0 0 2px rgba(120, 92, 48, .35),
    inset 0 2px 6px rgba(255, 255, 255, .6),
    inset 0 -6px 12px rgba(120, 92, 48, .18),
    0 6px 14px rgba(60, 44, 18, .3);
  transition: transform .18s ease, border-color .18s ease, box-shadow .18s ease;
}
.guide-med:hover .guide-med-ring {
  transform: translateZ(0) translateY(-3px) scale(1.04);
  box-shadow:
    0 0 0 2px rgba(120, 92, 48, .45),
    inset 0 2px 6px rgba(255, 255, 255, .6),
    inset 0 -6px 12px rgba(120, 92, 48, .18),
    0 12px 22px rgba(60, 44, 18, .38);
}
/* The images inside never scale — only their frame does. A scaled bitmap
   re-samples every frame and that is the shimmer. */
.guide-med-ring img, .guide-four-tab-ico, .guide-fact-ico { transform: translateZ(0); }
/* Opacity is composited; saturate() is not. Same visual read, no repaint. */
.guide-four-tab, .guide-step-tab { filter: none; transition: opacity .18s ease, transform .18s ease; }
.guide-four-tab:hover, .guide-step-tab:hover { filter: none; }
.guide-four-tab.on { transform: translateZ(0) translateX(5px); }
.guide-step-tab.on { transform: translateZ(0) translateY(-2px); }
/* The captures are large bitmaps in a 9-slice frame; nothing about them should
   move on hover at all. */
.guide-four-board, .guide-four-shot, .guide-shot img, .landing-art-shot img {
  transition: none;
}
@media (prefers-reduced-motion: reduce) {
  .guide-med-ring, .guide-four-tab, .guide-step-tab { transition: none; }
}

/* ── The Inbox (Aug 13 2026) ───────────────────────────────────────────────
   "create proper email handling menu in admin panel, where I can create new
    inboxes, recieve messages, read them, reply to them, etc."

   Three columns — addresses, messages, conversation — which is the layout every
   mail client converged on for good reason: it lets you triage without
   navigating. On a narrow screen the columns stack instead of shrinking to
   uselessness. */
.mail-layout {
  display: grid;
  grid-template-columns: 190px 300px minmax(0, 1fr);
  gap: 14px;
  min-height: 520px;
  align-items: start;
}
@media (max-width: 1100px) {
  .mail-layout { grid-template-columns: 160px minmax(0, 1fr); }
  .mail-read { grid-column: 1 / -1; }
}
@media (max-width: 720px) {
  .mail-layout { grid-template-columns: minmax(0, 1fr); }
}

.mail-side-title { color: #7a634e; margin-bottom: 6px; }
.mail-boxes { display: flex; flex-direction: column; gap: 3px; }
.mail-box {
  -webkit-appearance: none; appearance: none;
  display: grid; grid-template-columns: 1fr auto; gap: 2px 6px;
  text-align: left; cursor: pointer;
  padding: 7px 9px; border-radius: 7px;
  border: 1px solid transparent; background: none;
  font: inherit; color: var(--on-surface, #2b2114);
}
.mail-box:hover { background: rgba(122, 99, 78, .10); }
.mail-box.on { background: rgba(212, 160, 23, .16); border-color: rgba(212, 160, 23, .5); }
.mail-box-name { font-weight: 600; font-size: .92rem; }
.mail-box-addr { grid-column: 1 / -1; font-size: .74rem; color: #7a634e; overflow: hidden; text-overflow: ellipsis; }
.mail-badge {
  background: var(--gold, #d4a017); color: #241a08;
  border-radius: 9px; padding: 0 6px; font-size: .72rem; font-weight: 700; align-self: start;
}
.mail-add { margin-top: 8px; }
.mail-hint { font-size: .76rem; margin-top: 6px; line-height: 1.4; }

.mail-list { border-left: 1px solid rgba(122, 99, 78, .22); border-right: 1px solid rgba(122, 99, 78, .22); min-height: 200px; }
.mail-listbar { display: flex; gap: 8px; align-items: center; padding: 0 10px 8px; flex-wrap: wrap; }
.mail-search { flex: 1 1 130px; min-width: 0; }
.mail-toggle { font-size: .78rem; color: #5b4630; display: flex; align-items: center; gap: 4px; white-space: nowrap; }
.mail-item {
  -webkit-appearance: none; appearance: none;
  display: block; width: 100%; text-align: left; cursor: pointer;
  padding: 9px 11px; border: none; border-top: 1px solid rgba(122, 99, 78, .16);
  background: none; font: inherit; position: relative;
}
.mail-item:hover { background: rgba(122, 99, 78, .07); }
.mail-item.on { background: rgba(212, 160, 23, .14); }
/* Unread is carried by weight and a marker rather than colour alone, so it
   survives being printed, being read by someone colour-blind, and the parchment
   palette this admin sits on. */
.mail-item.unread .mail-from,
.mail-item.unread .mail-subject { font-weight: 700; }
.mail-item.unread::before {
  content: ''; position: absolute; left: 3px; top: 14px;
  width: 5px; height: 5px; border-radius: 50%; background: var(--gold, #d4a017);
}
.mail-item-top { display: flex; justify-content: space-between; gap: 8px; font-size: .84rem; }
.mail-from { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.mail-when { color: #7a634e; font-size: .76rem; white-space: nowrap; }
.mail-subject { font-size: .88rem; margin-top: 1px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.mail-count { background: rgba(122, 99, 78, .22); border-radius: 8px; padding: 0 5px; font-size: .72rem; }
.mail-snip { font-size: .78rem; margin-top: 2px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.mail-clip { font-size: .72rem; color: #7a634e; }
.mail-more { padding: 8px 11px; font-size: .78rem; }

/* ── Triage (Sep 17 2026) ──────────────────────────────────────────────────
   A tick box and a flag either side of the message, and a bar of actions that
   only exists while something is ticked. The row carries the divider now, so
   the line runs the full width of the list rather than stopping where the
   message text starts. */
.mail-row { display: flex; align-items: flex-start; gap: 6px; padding-left: 8px;
  border-top: 1px solid rgba(122, 99, 78, .16); }
.mail-row.picked { background: rgba(212, 160, 23, .10); }
.mail-row .mail-item { flex: 1 1 auto; min-width: 0; border-top: none; padding-left: 4px; }
.mail-pick { margin-top: 12px; flex: 0 0 auto; cursor: pointer; }
.mail-star {
  -webkit-appearance: none; appearance: none;
  border: none; background: none; cursor: pointer; font-size: .95rem;
  padding: 10px 7px 0 0; color: rgba(122, 99, 78, .32); line-height: 1;
}
.mail-star:hover { color: rgba(212, 160, 23, .75); }
.mail-star.on { color: var(--gold, #d4a017); }
.mail-all { margin-right: 2px; }
.mail-bulk { display: flex; flex-wrap: wrap; align-items: center; gap: 5px; padding: 0 10px 8px; }
.mail-bulk:empty { display: none; }
.mail-bulk-n { font-size: .78rem; color: #7a634e; margin-right: 2px; }
.mail-move { font: inherit; font-size: .8rem; padding: 3px 5px; }

.mail-reply-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; flex-wrap: wrap; }
.mail-reply-modes { display: flex; gap: 4px; }
/* The chosen mode is marked, because Reply and Reply All differ only in what
   they put in the Cc line — and that line stays editable afterwards. */
.mail-reply-modes .btn.on { background: rgba(212, 160, 23, .22); border-color: rgba(212, 160, 23, .6); }
.mail-field { margin-top: 7px; }

/* Files waiting to go out. Each chip is one upload that already succeeded, so
   what is on screen is what will be sent — no queue, no progress bar to
   misread. */
.mail-attach { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; margin-top: 9px; }
.mail-attach-add { cursor: pointer; }
.mail-attach-note { font-size: .78rem; }
.mail-attach-list { display: flex; flex-wrap: wrap; gap: 5px; flex-basis: 100%; }
.mail-chip {
  display: inline-flex; align-items: center; gap: 5px;
  border: 1px solid rgba(122, 99, 78, .3); border-radius: 13px;
  padding: 3px 5px 3px 9px; font-size: .78rem; background: rgba(253, 250, 242, .7);
}
.mail-chip-x {
  -webkit-appearance: none; appearance: none; border: none; background: none;
  cursor: pointer; font-size: 1rem; line-height: 1; padding: 0 3px; color: #7a634e;
}
.mail-chip-x:hover { color: #8d2b2b; }

.mail-read { padding: 0 4px 8px; min-width: 0; }
.mail-head { display: flex; justify-content: space-between; align-items: flex-start; gap: 10px; flex-wrap: wrap; margin-bottom: 8px; }
.mail-head h3 { margin: 0; font-size: 1.15rem; }
.mail-head-actions { display: flex; gap: 5px; flex-wrap: wrap; }
.mail-msg {
  border: 1px solid rgba(122, 99, 78, .22); border-radius: 9px;
  padding: 10px 12px; margin-bottom: 10px; background: rgba(253, 250, 242, .55);
}
/* A reply is indented and tinted so a conversation can be read at a glance as
   an exchange rather than a list. */
.mail-msg.out { margin-left: 22px; background: rgba(212, 160, 23, .09); border-color: rgba(212, 160, 23, .35); }
.mail-msg header { display: flex; gap: 8px; align-items: baseline; flex-wrap: wrap; font-size: .86rem; }
.mail-to { font-size: .76rem; margin-bottom: 6px; }
.mail-text { white-space: pre-wrap; word-break: break-word; font: inherit; font-size: .88rem; margin: 0; }
.mail-frame { width: 100%; min-height: 220px; border: 1px solid rgba(122, 99, 78, .2); border-radius: 6px; background: #fff; }
.mail-att {
  display: inline-block; margin: 6px 6px 0 0; padding: 4px 9px;
  border: 1px solid rgba(122, 99, 78, .3); border-radius: 6px;
  font-size: .8rem; text-decoration: none; color: inherit;
}
.mail-att:hover { border-color: var(--gold, #d4a017); }
.mail-err { margin-top: 6px; color: #8d2b2b; font-size: .82rem; }

.mail-reply { border-top: 2px solid rgba(212, 160, 23, .45); padding-top: 10px; margin-top: 4px; }
.mail-reply textarea { width: 100%; margin-top: 5px; font: inherit; font-size: .9rem; }
.mail-reply-actions { display: flex; align-items: center; gap: 10px; margin-top: 7px; }
.mail-reply-note { font-size: .8rem; }

/* ── A locked screen (Aug 15 2026) ─────────────────────────────────────────
   "Untill that - the menu of joining is pale and it has a lock in the middle of
    the screen with requirments."

   The feature stays on screen and goes pale. Showing a locked thing is how a
   player learns what they are working towards; a blank "come back later" page
   teaches them nothing. */
.locked-wrap { position: relative; min-height: 60vh; }

.locked-content {
  /* Blurred, and out of reach. Aug 18 2026 — "remove dark overlay screen. Make
     it just blury." It used to be faded to a third of its opacity, drained of
     colour and dimmed by a brown wash on top, which between them turned the
     screen to mud: a player could see there was something there but not what.
     Blur alone keeps the colour and the shapes, so the Koinon still looks like
     a place worth reaching.

     pointer-events alone would still let a keyboard user tab into the buttons
     underneath, which is how a locked screen quietly stops being locked —
     `inert` is set on the element in app.js as well. */
  filter: blur(6px);
  pointer-events: none;
  user-select: none;
}

.locked-veil {
  position: absolute; inset: 0;
  display: flex; align-items: center; justify-content: center;
  padding: 20px;
  /* No wash of its own. The blur behind is what separates the card from the
     page; darkening on top of it was the "dark overlay" that had to go. */
  background: none;
  z-index: 5;
}
/* On a tall page the lock follows you down rather than being stranded at the
   top of a screen's worth of dimmed content. */
@media (min-height: 520px) {
  .locked-veil { align-items: flex-start; }
  /* --lock-top is measured in app.js from the floating resource bar, which is
     taller on a phone than on a desktop; 22vh is the fallback if it is ever
     absent. */
  .locked-card { position: sticky; top: var(--lock-top, 22vh); }
}

/* On a phone the resource bar can take half the screen. The card is trimmed to
   suit rather than being pushed off the bottom of it. */
@media (max-width: 560px), (max-height: 720px) {
  /* The scroll's rollers and torn shoulders are 50-60px of the card at full
     size, which on a phone would leave the notice in a gutter. */
  .panel.locked-card {
    border-width: 44px 34px 46px 34px;
    border-image-width: 44px 34px 46px 34px;
    padding: 0 8px;
  }
  .locked-card h2 { font-size: 1.08rem; margin: 2px 0 9px; line-height: 1.25; }
  .locked-icon { margin-bottom: 0; }
  .locked-icon img { width: 96px; height: 62px; }
  .locked-reqs { margin-bottom: 9px; }
  .locked-reqs li { font-size: .86rem; padding: 6px 9px; }
  .locked-note { font-size: .78rem; margin-bottom: 11px; line-height: 1.35; }
}

/* Aug 18 2026 — "put info befire acropolis lvl 5 in kainon in scroll instead
   of white background." The requirement is a notice, and notices in this game
   are written on parchment; a plain cream card was the one thing on the screen
   that did not belong to the world.

   `.panel.locked-card`, not `.locked-card`: `.panel` carries the cream
   background and is declared later in this file, so a single class ties on
   specificity and loses on source order — the same trap documented at
   .panel.account-panel. */
.panel.locked-card {
  max-width: 470px; width: 100%;
  text-align: center;
  border: solid transparent;
  border-width: 58px 50px 60px 50px;
  border-image: url('/assets/ui/scroll_frame.png?v=49') 175 160 175 160 fill / 58px 50px 60px 50px stretch;
  border-radius: 0;
  background: none;
  /* The torn parchment edge wanders further in than the border width does, so
     the frame alone never guarantees a margin — this padding does. */
  padding: 0 18px;
  /* drop-shadow, not box-shadow: the scroll is a torn silhouette with rollers
     that overhang its box, and box-shadow would trace the rectangle instead of
     the parchment. */
  box-shadow: none;
  filter: drop-shadow(0 14px 34px rgba(16, 11, 4, .5));
  /* max-height is set in app.js from the room left under the resource bar. */
  overflow-y: auto;
}
/* Ink on parchment. */
.panel.locked-card h2 { color: #2a2013; }
.panel.locked-card .muted { color: #6d5a3e; }
.panel.locked-card::before, .panel.locked-card::after { display: none; }
.locked-card h2 { margin: 4px 0 12px; font-size: 1.3rem; }

/* A building render rather than an icon, so it is given room to read as one. */
.locked-icon { margin-bottom: 4px; }
.locked-icon img {
  width: 148px; height: 118px; object-fit: contain;
  filter: drop-shadow(0 6px 16px rgba(28, 20, 10, .45));
}

.locked-reqs {
  list-style: none; margin: 0 0 14px; padding: 0;
  display: flex; flex-direction: column; gap: 7px;
}
.locked-reqs li {
  font-size: .95rem; font-weight: 600;
  padding: 8px 12px;
  border: 1px solid rgba(212, 160, 23, .45);
  border-radius: 8px;
  background: rgba(212, 160, 23, .10);
}
.locked-note { font-size: .85rem; margin: 0 0 16px; line-height: 1.45; }

/* The "no Koinon, no war" placeholder. Aug 18 2026 — "remove white background
   from in war and put it in wooden frame."
   The same carved board the war fronts sit on (.war-board above), so the empty
   state belongs to the war page instead of being a cream card dropped onto it.
   `.panel` carries the cream background and is declared later in this file, so
   the selector has to be `.panel.war-empty` — a single class ties on
   specificity and loses on source order, which is the trap already documented
   at .panel.account-panel. */
.panel.war-empty {
  max-width: 520px; margin: 24px auto 0;
  border: solid transparent;
  border-width: 31px 52px 32px 53px;
  border-image-source: url('/assets/ui/frame_wood.png?v=49');
  border-image-slice: 68 116 70 118;
  border-image-width: 31px 52px 32px 53px;
  border-image-repeat: round round;
  background: url('/assets/ui/wood_panel.jpg?v=49') center / cover no-repeat #3d2a19;
  background-clip: padding-box;
  border-radius: 0;
  box-shadow: none;
  filter: drop-shadow(0 14px 34px rgba(0, 0, 0, .55));
}
/* Dark planks want light ink. */
.panel.war-empty h2 { color: #f6ecd2; text-shadow: 0 2px 5px rgba(0, 0, 0, .8); }
.panel.war-empty .muted { color: #cbb691; }
.panel.war-empty img { filter: drop-shadow(0 4px 12px rgba(0, 0, 0, .6)); }
@media (max-width: 720px) {
  .panel.war-empty {
    border-width: 18px 30px 19px 31px;
    border-image-width: 18px 30px 19px 31px;
  }
}

