/* site.css — small extensions to victoria.css for site-shell components the
   mock doesn't define: the authenticated user's dropdown menu, the Django
   messages framework, and the stale-data banner (docs/PAGES.md §0).
   victoria.css itself is ported verbatim and never forked (AGENTS.md §6);
   new components live here instead, using the same `v-` vocabulary and the
   existing design tokens. */

/* Alpine hides x-cloak elements only once it boots; this rule hides them
   before that so the user menu never flashes open on load. */
[x-cloak] {
  display: none !important;
}

/* `.v-btn`'s own `display: inline-flex` (victoria.css) and the browser's
   default `[hidden] { display: none }` rule have equal CSS specificity,
   and an author stylesheet always beats the user-agent one — so a hidden
   `.v-btn` stayed visible regardless of source order (found via the
   no-JS screenshot check for the map panel's collapse/expand toggle,
   feedback-2026-08-18 WO-F4.1; also affects map-viewer.js's pre-existing
   fullscreen-button hide when the browser has no Fullscreen API). */
.v-btn[hidden] {
  display: none !important;
}

/* --- User chip dropdown --------------------------------------------------- */

.v-user-menu {
  position: relative;
}
.v-user-menu img.v-avatar {
  object-fit: cover;
}
.v-user-menu__panel {
  position: absolute;
  top: calc(100% + 8px);
  right: 0;
  z-index: 60;
  min-width: 170px;
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: var(--v-2);
  background: var(--v-panel-dark-2);
  border: var(--v-border);
  border-radius: var(--v-radius);
  box-shadow: var(--v-shadow-lg);
}
.v-user-menu__panel form {
  margin: 0;
}
.v-user-menu__item {
  display: block;
  width: 100%;
  padding: var(--v-2) var(--v-3);
  font-family: var(--v-font-body);
  font-size: var(--v-fs-sm);
  color: var(--v-cream);
  text-align: left;
  background: transparent;
  border: none;
  border-radius: var(--v-radius);
  cursor: pointer;
}
.v-user-menu__item:hover {
  background: rgba(194, 161, 90, 0.16);
  color: var(--v-gold-bright);
  text-decoration: none;
}

/* --- Django messages framework -------------------------------------------- */

.v-messages {
  padding-block: var(--v-4) 0;
  display: flex;
  flex-direction: column;
  gap: var(--v-2);
}
.v-message {
  padding: var(--v-3) var(--v-4);
  border: 1px solid var(--v-gold-dim);
  border-left-width: 4px;
  border-radius: var(--v-radius);
  background: var(--v-panel-dark);
  color: var(--v-cream);
  font-size: var(--v-fs-sm);
}
.v-message--success {
  border-left-color: var(--v-green);
}
.v-message--error {
  border-left-color: var(--v-red);
}
.v-message--warning {
  border-left-color: var(--v-gold);
}
.v-message--info,
.v-message--debug {
  border-left-color: var(--v-blue);
}

/* --- Footer health line (PAGES.md §0) ------------------------------------- */

.v-health {
  margin: 0 0 var(--v-2);
}
.v-health__dot {
  display: inline-block;
  width: 8px;
  height: 8px;
  border-radius: 50%;
  margin-right: 4px;
  vertical-align: baseline;
}
.v-health__dot--live {
  background: var(--v-green);
}
.v-health__dot--refreshing {
  background: var(--v-gold);
}
.v-health__dot--unavailable {
  background: var(--v-cream-2);
  opacity: 0.5;
}

/* --- Stale-data banner (PAGES.md §0) -------------------------------------- */

.v-stale-banner {
  padding: var(--v-3) var(--v-4);
  text-align: center;
  font-size: var(--v-fs-sm);
  color: var(--v-cream);
  background: var(--v-maroon-2);
  border-bottom: 1px solid var(--v-gold-dim);
}

/* --- Notice-page shell (error pages, interstitials) ------------------------ */

/* The centred framed-card layout the mock uses inline on its 404; shared by
   the error pages so the shell isn't copy-pasted per template. */
.v-notice-page {
  display: grid;
  place-items: center;
  min-height: 70vh;
}
.v-notice-page > .v-paper {
  max-width: 520px;
}

/* --- Pagination (templates/common/pagination.html) ------------------------- */

.v-pager {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--v-2);
  padding-block: var(--v-4);
  flex-wrap: wrap;
}
.v-pager__pages {
  display: inline-flex;
  align-items: center;
  gap: var(--v-1);
}
.v-pager__current {
  min-width: 28px;
  text-align: center;
}
.v-pager__ellipsis {
  color: var(--v-ink-2);
  padding-inline: var(--v-1);
}

/* === Phase 2: timeline — owned by apps/timeline ========================== */

/* The whole card is the expand affordance (PAGES.md §2 "cards are
   clickable"): the title link stretches over the card, and the elements
   listed in the lift rule below sit above the stretched hit area.
   **A second copy of this pattern lives in victoria.css** as
   `.v-home-event__card`/`.v-home-event__title a::after` (the home Recent
   Events card, 2026-08-24b) — the two lift different element sets, so the
   pattern was duplicated rather than abstracted; see that block's comment
   (which also records why the *title* anchor must not be given
   `position: relative` there). A change to the overlay geometry here has
   to be made there too. */
.v-event__card {
  position: relative;
}
/* **The title anchor must stay `position: static`** — `inset: 0` resolves
   against the pseudo-element's containing block, i.e. its nearest
   non-static ancestor, so positioning the anchor re-roots this overlay
   onto the anchor's own inline box and the "stretched" hit area collapses
   to the width of the title text. That is exactly what it did here from
   this pattern's introduction until 2026-08-24b: `.v-event__title a` was
   in the lift rule below, so a click anywhere on a collapsed card except
   the title itself hit nothing at all, while PAGES.md §2 and this
   comment both said the whole card was the expand affordance (QA
   gatekeeper 2026-08-24b, proven with `elementFromPoint` and a real
   click — the identical trap was caught the same round in this pattern's
   younger copy on the home card). The anchor needs no lift of its own:
   the overlay *is* its pseudo-element, so a click on the overlay
   activates that very link.

   Scoped to the **collapsed** card. `.v-event__title` is shared with
   `_event_expanded.html`, whose title link *collapses* the card — an
   overlay stretched across an expanded card would turn every click on
   its prose, chips and images into a collapse. The expanded card gets no
   overlay at all and therefore needs no exception to the rule above. */
.v-event__card:not(.v-event__card--expanded) .v-event__title a::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 0;
}
.v-event__card .v-btn,
.v-event__byline a {
  position: relative;
  z-index: 1;
}

/* `.v-event__byline a` (the submitter's nation link, `byline.username_url`)
   was missing from the lift above until 2026-08-24b — found while working
   this round's home-card goal, approved by the owner to ride along
   (feedback-2026-08-24b, "Found along the way"). `.v-event__title
   a::after { inset: 0 }` stretches its overlay across the whole positioned
   `.v-event__card`, so anything else inside the card that isn't lifted
   above it on `z-index: 1` is visually present but unclickable — a click
   anywhere on the card, including on the byline link, hits the stretched
   overlay first and fires the card's own expand navigation instead. Only
   the title link and the buttons were ever raised above it; the byline
   link sat at the overlay's `z-index: 0` and was covered. The home
   Recent Events card carries the identical lift for the identical
   reason (`.v-home-event__body .v-event__byline a`, victoria.css) — the
   same round moved it onto this same stretched-link shape. It was not
   affected before that only because its byline sat outside the card-wide
   `<a>` the old shape wrapped everything else in; that shape is gone, so
   both cards now need the lift. */

/* Era header — breaks out of the year gutter to span the full spine width,
   marking where the stream crosses into the era's span (PAGES.md §2). */
.v-timeline__era {
  position: relative;
  margin: var(--v-6) 0 var(--v-4) -92px;
  padding: var(--v-3) var(--v-4);
  background: rgba(43, 36, 28, 0.06);
  border-top: var(--v-border);
  border-bottom: var(--v-border);
}
.v-timeline__era:first-child {
  margin-top: 0;
}
.v-timeline__era-title {
  margin: 0;
  font-family: var(--v-font-display);
  font-size: var(--v-fs-lg);
}
.v-timeline__era-years {
  font-size: var(--v-fs-sm);
  color: var(--v-ink-3);
  font-weight: normal;
}
.v-timeline__era-desc {
  margin-top: var(--v-2);
  font-size: var(--v-fs-sm);
  color: var(--v-ink-2);
}

/* Soft-deleted events, staff view only (PAGES.md §2) — dimmed, never hidden
   from the staff who need the Restore button. */
.v-event--hidden .v-event__card {
  opacity: 0.55;
}
.v-tag--muted {
  color: var(--v-ink-3);
}

/* A real uploaded thumbnail dropped into the mock's placeholder slot. */
img.v-event__thumb {
  object-fit: cover;
  border-radius: var(--v-radius);
}

/* Expanded card: room for the full body, image, and staff actions. */
.v-event__card--expanded {
  flex-direction: column;
}
.v-event__image {
  display: block;
  max-width: 100%;
  height: auto;
  border-radius: var(--v-radius);
  margin: var(--v-3) 0;
}
.v-event__meta {
  margin: 0 0 var(--v-2);
}
.v-event__prose :is(p, ul, ol, blockquote, table) {
  margin: 0 0 var(--v-3);
}
.v-event__prose img {
  max-width: 100%;
  height: auto;
  border-radius: var(--v-radius);
}

/* The inline add/edit form card mirrors the mock's admin panel fields
   inside a timeline card instead of a separate panel (edit replaces the
   card in place — PAGES.md §2). */
.v-event__card--form {
  flex-direction: column;
}
.v-field-error {
  color: var(--v-red);
  font-size: var(--v-fs-xs);
}

/* Events filter toolbar's region checkbox group (WO-H, phase-5.5 R4-14) —
   replaces the old `<select multiple>`, which a plain click could never
   fully unselect. `.v-row` (victoria.css) already wraps, but a bare
   `width:auto` field can grow as wide as the whole panel body before any
   wrapping kicks in, so cap the group's width itself — ten seeded regions
   today, more expected in production (PAGES.md §2) — so a long list
   breaks into several short lines inside the collapsible panel instead of
   one wide ragged row. */
.v-filter-regions {
  max-width: 420px;
  gap: var(--v-1) var(--v-4);
}
.v-filter-regions label {
  display: inline-flex;
  align-items: center;
  gap: var(--v-1);
  font-size: var(--v-fs-sm);
  white-space: nowrap;
}

@media (max-width: 820px) {
  .v-timeline__era {
    margin-left: calc(-1 * var(--v-5));
  }
}

/* === Phase 2: world map viewer — owned by apps/world ===================== */

/* The pan/zoom container: a fixed-aspect frame that clips the zoomed/panned
   stage. panzoom (static/js/map-viewer.js) attaches to `__stage` and drives
   it with a CSS transform; the frame just clips and anchors the controls. */
.v-map-viewer__frame {
  position: relative;
  overflow: hidden;
  border-radius: var(--v-radius);
  background: #0b0906;
  aspect-ratio: 16 / 9;
  max-height: 78vh;
}
.v-map-viewer__stage {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  transform-origin: 0 0;
  cursor: grab;
  touch-action: none;
}
.v-map-viewer__stage:active {
  cursor: grabbing;
}
.v-map-viewer__img {
  display: block;
  width: 100%;
  height: auto;
  user-select: none;
  -webkit-user-drag: none;
}
.v-map-viewer__controls {
  position: absolute;
  right: var(--v-3);
  bottom: var(--v-3);
  z-index: 2;
  display: flex;
  gap: var(--v-2);
}
.v-map-viewer__controls .v-btn {
  min-height: 36px;
  padding: var(--v-1) var(--v-3);
}
.v-map-panel__caption {
  margin: var(--v-2) 0 0;
}

/* The map panel's old cropped-strip collapse (PAGES.md §3.1,
   feedback-2026-08-18 WO-F4.1) is gone — phase-5.5 fourth round replaced
   it with a real `<details>`/`<summary>` (`{% collapse_panel %}`, see the
   "Collapsible panel — shared" block below), which hides the whole body
   natively instead of cropping it to a fixed-height strip. Nothing sets
   `.v-map-panel--collapsible` or `.is-collapsed` any more. */

/* Fullscreen: the frame fills the viewport; the small vendor-prefixed
   selectors cover older WebKit (map-viewer.js falls back the same way). */
.v-map-viewer__frame:fullscreen,
.v-map-viewer__frame:-webkit-full-screen {
  aspect-ratio: auto;
  max-height: none;
  width: 100vw;
  height: 100vh;
  border-radius: 0;
}

/* The era-maps strip (contract with apps/timeline — templates/world/era_maps.html) */
.v-era-maps {
  display: flex;
  flex-wrap: wrap;
  gap: var(--v-3);
}
.v-era-maps__item {
  display: flex;
  align-items: center;
  gap: var(--v-2);
  padding: var(--v-1) var(--v-2);
  border: 1px solid var(--v-gold-dim);
  border-radius: var(--v-radius);
  text-decoration: none;
  color: inherit;
  background: rgba(0, 0, 0, 0.15);
}
.v-era-maps__item:hover {
  border-color: var(--v-gold);
  text-decoration: none;
}
.v-era-maps__thumb {
  width: 64px;
  height: 40px;
  object-fit: cover;
  border-radius: 2px;
  display: block;
  flex: none;
}
.v-era-maps__meta {
  display: flex;
  flex-direction: column;
  gap: 2px;
}
.v-era-maps__label {
  font-family: var(--v-font-display);
  font-size: var(--v-fs-sm);
}

/* --- Collapsible panel — shared (WO-F9 feedback-2026-08-18; generalized
   phase-5.5 fourth round, "allow minimizing the world map and the
   leaderboard like we can minimize the filter tab") ----------------------- */
/* Two components share this native-<details>-based collapse: the original
   filter toolbar (`.v-filter-toggle`, apps/common/templatetags/
   filter_toolbar.py — world directory, character database, timeline) and
   the newer generic panel (`.v-collapse`, apps/common/templatetags/
   collapse_panel.py — the world map panel and the Leaderboards section).
   Both wrap a page's own content in <details>/<summary> so it starts
   hidden behind one clickable head, no JS required — a no-JS visitor gets
   a real, always-openable control either way. Built on `.v-panel`/
   `.v-panel__head`/`.v-panel__body` (victoria.css) for the frame/surface;
   only the summary's disclosure triangle (and, for the filter toolbar
   only, its count badge) are new.

   The two components render genuinely different classes on purpose —
   `filter_toolbar.py`'s markup (and this file's rules for it) predate
   `collapse_panel.py` and are left exactly as WO-F9 shipped them, so nothing
   about the filter toolbar's rendered appearance changes here — but the
   caret/marker/summary mechanics are identical, so those rules are grouped
   under both selectors together below instead of being duplicated as a
   second, separate block. Only `.v-filter-toggle` gets the count badge
   rule at the end: neither `.v-collapse` caller has a count to show. */
details.v-filter-toggle,
details.v-collapse {
  overflow: hidden;
}
.v-filter-toggle__summary,
.v-collapse__summary {
  cursor: pointer;
  user-select: none;
  list-style: none;
}
.v-filter-toggle__summary::-webkit-details-marker,
.v-collapse__summary::-webkit-details-marker {
  display: none;
}
.v-filter-toggle__summary::marker,
.v-collapse__summary::marker {
  content: "";
}
.v-filter-toggle__caret,
.v-collapse__caret {
  display: inline-block;
  flex: none;
  width: 0.6em;
  transition: transform 0.15s ease;
  transform: rotate(0deg);
}
.v-filter-toggle__caret::before,
.v-collapse__caret::before {
  content: "\25B8"; /* ▸ */
}
details.v-filter-toggle[open] > .v-filter-toggle__summary .v-filter-toggle__caret,
details.v-collapse[open] > .v-collapse__summary .v-collapse__caret {
  transform: rotate(90deg);
}
.v-filter-toggle__summary:focus-visible,
.v-collapse__summary:focus-visible {
  outline: 2px solid var(--v-gold-bright);
  outline-offset: -2px;
}
/* Decorative pill echoing the count that already sits in the summary's own
   text ("Filters (3)") — reinforcement for a sighted user skimming the
   page, never the only place the number appears. A real `aria-hidden`
   `<span>` (filter_toolbar.py), not CSS `content: attr()`: Chromium folds
   generated pseudo-element content into the accessible name, which would
   have announced the count twice ("Filters (1) 1" — caught via an
   accessibility-tree dump during WO-F9 QA). `aria-hidden` on a real node
   is the one way to keep it decoration-only. `.v-collapse` has no
   equivalent — its callers show a fixed title, not a count. */
.v-filter-toggle__badge {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 1.4em;
  padding: 0 5px;
  margin-left: auto;
  border: 1px solid var(--v-gold-dim);
  border-radius: 999px;
  background: var(--v-gold-dim);
  color: var(--v-panel-dark);
  font-size: var(--v-fs-xs);
  font-weight: 700;
}

/* === Country sheet — owned by apps/world ================================= */

/* Overview pane's Nation/Economy-Ruler-PowerBloc-Government two-column
   layout (world/_country_overview.html, WO-F5) — previously an inline
   `grid-template-columns:minmax(220px,1fr) minmax(0,1.4fr)` on a bare
   `.v-grid`, which a media query cannot override (an element's own inline
   style always wins over any stylesheet rule short of `!important`). At
   375px the 220px minimum alone exceeds the available content width, so
   the second column was forced into ~90px: every info-box value wrapped
   one word per line and the trailing text was cut off entirely
   (`Cash 71...K`, `CONCE...RT OF THE ATLANT...` — feedback-2026-08-18
   mobile pass, USA/SWE/GBR sheets). A named class moves the two-column
   layout into the stylesheet where the existing `.v-shell`-style pattern
   (victoria.css §16, "Responsive") can collapse it to one column below
   the tablet breakpoint. */
.v-overview-grid {
  grid-template-columns: minmax(220px, 1fr) minmax(0, 1.4fr);
}
@media (max-width: 820px) {
  .v-overview-grid {
    grid-template-columns: minmax(0, 1fr);
  }
}

/* Hero CTA row (`content/partials/hero.html`, phase-5.5 item R4-3, owner
   decision) — "Join the campaign" moves to the top on a phone-width
   viewport, desktop order unchanged. Two of the row's three buttons
   ("Join", "Download the Mod") are conditionally rendered, so the DOM is
   `[Read, Join, Download]`, `[Read, Download]`, or `[Read, Join]`
   depending on viewer/config state — an `:nth-child()` `order` rule would
   therefore pick the wrong element in at least one state. `order` is keyed
   to `.v-hero-cta__join`, a class on the Join button itself, so it is
   correct in every DOM state regardless of which siblings rendered; the
   other two buttons need no class at all. `justify-content:center` was
   moved here from the row's inline `style=` — an inline style outranks any
   stylesheet rule, so leaving it inline would have blocked this override. */
.v-hero-cta {
  justify-content: center;
}
@media (max-width: 520px) {
  .v-hero-cta {
    flex-direction: column;
    align-items: stretch;
  }
  .v-hero-cta__join {
    order: -1;
  }
}

/* Pagination links (`.v-pager`, templates/common/pagination.html) reuse
   `.v-btn--sm` (min-height 34px), fine with a mouse but tight for a thumb
   once the "1"/"2"-style single-digit pages sit only `var(--v-1)` (4px)
   apart — confirmed via bounding-box measurement at 375px (single-digit
   page links measured ~33x34px). Scoped to `.v-pager` so the very common
   `.v-btn--sm` elsewhere (inline card actions, admin bar) keeps its
   desktop-tuned size. */
@media (max-width: 820px) {
  .v-pager { gap: var(--v-3); }
  .v-pager__pages { gap: var(--v-2); }
  .v-pager .v-btn--sm {
    min-height: 44px;
    min-width: 44px;
    padding-inline: var(--v-3);
  }
}

/* R5-3 (owner, 2026-08-19): a Goals-pane trait/role chip
   (`templates/council/_goal_card_body.html`'s `.v-goal-tag`, only the
   role/trait tags — not the "Rewarded"/"Locked" chips beside them, which
   are always short) could overflow its card. Most of
   that length is now gone (`apps.council.pane_goals._collapse_tier_group`
   turns a tiered trait ladder into one base label), but a genuinely
   disjoint trait/role group can still run long, and the fix must hold for
   that case regardless of the wording. Root cause: `.v-tag` in
   victoria.css sets `white-space: nowrap`, which is right for the SHORT
   chips it was designed for (a role name, a country tag) but forces a
   long joined chip to keep its full one-line width no matter how narrow
   its flex row is — the containing `.v-row` already wraps chips onto new
   lines (`flex-wrap: wrap`, victoria.css), it just can't wrap the TEXT
   inside one chip that is itself too wide.

   `.v-tag` itself is the ported design system and must not be forked
   (AGENTS.md §6) — a global `white-space: normal` there would also let
   every short chip sitewide (roster role chips, country-tag chips,
   category tags) reflow it never needs to. `.v-goal-tag` is the
   project-authored class the goal-card template adds alongside `.v-tag`
   for exactly its long trait/role chips, so this rule reaches only them:
   `white-space: normal` lets the chip's own text wrap, and `max-width:
   100%` (with the flex item's `min-width` reset off its nowrap-driven
   intrinsic width) stops the chip claiming more width than its row has,
   which is what "overflow" meant here — a chip wider than the card,
   spilling past its right edge, rather than wrapping down. */
.v-goal-tag {
  white-space: normal;
  min-width: 0;
  max-width: 100%;
}

/* Home page body (`templates/home.html`, item R5-4, owner decision
   2026-08-19) — Campaign Overview and Recent Events side by side on
   desktop instead of one full-width stack. The fourth round removed the
   home page's only other two-column grid (banner beside the news list)
   when the banner image option itself was cut, leaving recent-events as a
   lone very-wide column of narrow cards — "too spread […] too much
   whitespace" (owner). News gets the wider share: its cards carry an
   image, a title, tagged-nation flags and a summary and want room to
   breathe, where the overview is a dense grid of short number tiles that
   reads fine narrow. Deliberately uneven, never a 50/50 split. Collapses
   to one column
   at the tablet breakpoint, same as every other two-column layout on the
   site (`.v-overview-grid` above). **The ratio is 1fr/2fr since
   2026-08-24b** — it read 2fr/3fr from this paragraph's own round until
   then; what survived the change is the reasoning, not the numbers (news
   still gets the wider share, deliberately uneven — see the ratio
   paragraph below for why it moved). `home.html` used to wrap the pair in
   this grid only when Campaign Overview would actually render
   (`show_figures` on, or edit mode) — with it off, Recent Events rendered
   alone at full width rather than sitting beside an empty column. **That
   condition was widened on 2026-08-23** and is no longer keyed to Campaign
   Overview alone; the next paragraph states what it is today.

   Third area added 2026-08-23 (docs/roadmap/feedback-2026-08-23.md, "fill
   the empty space under campaign overview") for the home Prestige
   Leaderboard panel (`.v-home-columns__prestige`,
   content/partials/prestige.html). Named areas rather than source order,
   because desktop and mobile need genuinely different orders from the
   same three DOM children — home.html keeps them in overview/news/
   prestige order for the mobile-matching a11y reason explained there.
   Desktop: Overview stacks above Prestige in the left column while
   Recent Events spans both rows on the right — literally "fill the
   empty space under campaign overview". Mobile: the areas restate the
   owner's requested Overview -> Events -> Prestige order. This is also
   why the prestige panel is a grid *sibling* of `__overview` and
   `__news`, not nested inside `__overview`: nesting would keep it in
   the left column at every width, making the mobile order (prestige
   after, not inside, the overview block) impossible to express.
   `home.html` now wraps the grid whenever Campaign Overview OR the
   prestige panel has something to show; an absent panel's own partial
   renders nothing (no wrapping element at all beyond the empty `<div
   class="v-home-columns__*">`), so its grid area just collapses to that
   div's zero height.

   **Ratio changed 2fr/3fr -> 1fr/2fr (40/60 -> ~33/67) on 2026-08-24**
   (docs/roadmap/feedback-2026-08-24b.md, owner: "the left column of the
   main page is too wide... could be more compact to leave more space for
   the events"). At the widest the page ever gets — `.v-container`'s
   1360px cap, 1328px inside its `--v-4` padding, less the grid's own
   `--v-4` gap — this moves the left column from ~525px to ~437px and
   hands Recent Events the ~87px difference. (Earlier drafts of this
   comment, and the session plan, said "~470px -> ~390px at a 1200px
   container" — that container width was assumed, not measured; the
   ratio change and the ~7% shift it buys are the same either way.) The two
   left-column panels (Campaign Overview's stat tiles, the Prestige table)
   get matching **home-scoped** density rules below this block, for the
   same reason the owner gave: narrowing the column without shrinking what
   sits inside it would just make the panels feel cramped, not compact.
   The council overview pane shares `.v-figure` with these panels, and
   `/world/`'s leaderboards, the roster and the country sheet share
   `.v-table` with them; all of those must render byte-identically to
   before, so every density rule below is scoped under
   `.v-home-columns__overview` / `.v-home-columns__prestige` rather than
   touching `.v-figure`/`.v-table` themselves.

   `grid-template-rows: min-content 1fr` is what keeps that collapse
   honest, and is NOT cosmetic (QA 2026-08-23, caught by screenshot — the
   test suite cannot see it). Both rows left `auto`, the `news` item
   spans them, so CSS Grid's "distribute a spanning item's extra height
   across the tracks it spans" step hands ~half of Recent Events' height
   to row 1 — even when row 1 is the *empty* overview area. With Campaign
   Overview hidden (`show_figures` off, edit mode off) the prestige panel
   was pushed ~125px down the left column, floating against nothing. A
   flexible row 2 takes that distribution instead (per the spec, a
   spanning item's extra space goes to the flexible tracks it spans),
   pinning row 1 to Campaign Overview's own height — its content height
   when it renders, zero when it does not. Do not "simplify" these rows
   back to `auto`.

   One residual, accepted rather than hacked around: with row 1 collapsed
   the row-gap still sits above the prestige panel, so it starts ~16px
   below Recent Events' top edge instead of flush with it. Zeroing
   `row-gap` and leaning on each partial's own `margin-bottom` is not the
   fix — `recent_events.html`'s root carries no margin of its own, so
   that would collapse the mobile Events -> Prestige spacing to nothing. */
.v-home-columns {
  grid-template-columns: minmax(0, 1fr) minmax(0, 2fr);
  grid-template-areas:
    "overview news"
    "prestige news";
  grid-template-rows: min-content 1fr;
  align-items: start;
}
@media (max-width: 820px) {
  .v-home-columns {
    grid-template-columns: minmax(0, 1fr);
    grid-template-areas:
      "overview"
      "news"
      "prestige";
    /* Back to `auto`: the desktop rule's `min-content 1fr` exists purely to
       stop the spanning `news` item inflating an empty row 1, and nothing
       spans anything here — three stacked rows, each sized by its own
       panel. Leaving the desktop value in place would pin row 1 (overview)
       to `min-content` and row 2 (news) to `1fr` while row 3 (prestige)
       fell back to `auto` — not what this layout means, even where it
       happens to look the same. */
    grid-template-rows: auto;
  }
}
.v-home-columns__overview { grid-area: overview; }
.v-home-columns__news     { grid-area: news; }
.v-home-columns__prestige { grid-area: prestige; }

/* Campaign Overview's tile grid (`.v-grid--4`, figures.html) is
   `auto-fit, minmax(180px,1fr)` (victoria.css), sized on the assumption of
   a full-width container. Nested in `.v-home-columns__overview` that
   reflows unpredictably across the desktop range: 2 tiles wide on a wide
   monitor, but a column too narrow for a second 180px tile plus gap
   between ~820px and ~900px of viewport width collapses it to a single
   stack of 8 full-width rows — squashed exactly where the two-column body
   is active and least expected. Forcing 2 columns (the same count the
   existing sub-520px rule in victoria.css already settles on, for the same
   reason) keeps the tile grid compact and even across that range.

   **Scoped to the widths where the two column layout is actually active**
   (review gatekeeper 2026-08-19): the wrapper `div` exists at every width,
   so an unconditional rule here also overrode the *single*-column layout
   below 820px, where `.v-home-columns__overview` is the full container
   width and `auto-fit` gives 3-4 tiles correctly — a silent tablet
   regression, and a contradiction of what this comment and PAGES.md §1
   both said the rule did. Below 520px victoria.css already pins 2 columns
   for its own (unrelated, still-correct) reason. */
@media (min-width: 821px) {
  .v-home-columns__overview .v-grid--4 {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}

/* Left-column panel density (feedback-2026-08-24b, same owner round as the
   1fr/2fr ratio change above) — the owner asked for the panels themselves
   to be tighter, not just for the column to be narrower. Both rules below
   are scoped to their home-page wrapper class, never to the shared `.v-figure`/
   `.v-table` selectors themselves: `.v-figure` renders on the council
   overview pane too (`templates/council/pane_overview.html`) and `.v-table`
   is the whole site's tabular component (world leaderboards, the roster,
   the country sheet's council/archive tables, reference pages) — an
   unscoped edit here would shrink all of those along with the home panels,
   which nothing in this round asked for and which the plan's success
   criteria explicitly forbid ("/world/ renders byte-identically to
   before"). */

/* Campaign Overview's stat tiles: one step less vertical padding
   (`.v-figure`'s global `var(--v-3) var(--v-2)`, victoria.css, drops its
   `--v-3` top/bottom to `--v-2` — horizontal padding is untouched) and one
   size step down on the number itself (global `var(--v-fs-xl)` ->
   `var(--v-fs-lg)`, the same step victoria.css's own `@media (max-width:
   520px)` rule already takes for the identical reason: `.v-figure__val` is
   the widest thing in the tile and the first to force a squeeze). */
.v-home-columns__overview .v-figure {
  padding: var(--v-2) var(--v-2);
}
.v-home-columns__overview .v-figure__val {
  font-size: var(--v-fs-lg);
}

/* The panel's two `.v-grid--4` tile rows are separated by an inline
   `style="margin-bottom:16px"` on the first one (figures.html) — an
   element's own inline style outranks any stylesheet rule short of
   `!important` (the same reasoning already documented above for
   `.v-overview-grid`), so the gap could not be tightened from here without
   forking that markup or resorting to `!important`. Changed in the
   template instead, from 16px to `--v-2` (8px) — see figures.html. */

/* Prestige Leaderboard table: one step less cell padding (global
   `var(--v-2) var(--v-3)` on `.v-table th, .v-table td`, victoria.css,
   steps down to `var(--v-1) var(--v-2)` — both the vertical and horizontal
   token drop one size, matching the tighter column it now sits in). */
.v-home-columns__prestige .v-table th,
.v-home-columns__prestige .v-table td {
  padding: var(--v-1) var(--v-2);
}

/* --- Character profile "Sheet"/Biography split (PAGES.md §4.2, owner ask
   2026-08-26) ----------------------------------------------------------

   The basic-details stat grid (`.v-grid--3`, victoria.css) used to span
   the full paper width on its own row, leaving the Biography block (new,
   this round) nowhere to sit but stacked below it. Scoped here rather
   than widening `.v-grid--3`/`.v-stat` themselves, the same reasoning
   `.v-home-columns` above gives for its own scoped density rules: those
   two classes are shared with every other stat grid on the site (the
   council pane, the dashboard, the country sheet), and must keep
   rendering byte-identically everywhere else. `.v-grid--3` nested inside
   the narrower left column still auto-fits on its own (`minmax(230px,
   1fr)`) — two per row at the column widths this page actually reaches —
   so nothing about it needs forcing the way the home page's `.v-grid--4`
   did against its own tighter overlapping breakpoint. */
.v-character-columns {
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  align-items: start;
}
@media (max-width: 720px) {
  .v-character-columns { grid-template-columns: minmax(0, 1fr); }
}

/* --- Red-dot badges (plan 2026-08-27 Goal 5, T6b) -------------------------

   A small filled circle marking "action needed" on the topbar Dashboard
   nav link (`templates/base.html`) and the dashboard rail's Council/Goals/
   Actions items (`templates/council/dashboard.html`). Reuses `--v-red`
   (victoria.css — the same token `.v-tag--red`'s `color` and the
   disloyalty-penalty/bypass-mode tags already use) rather than introducing
   a new hex. Colour alone never carries the meaning: every `.v-dot` is
   paired with `.v-sr-only` text at its use site so a screen reader hears
   "Dashboard (action needed)"/"Council (action needed)"/etc, not a silent
   cue (WCAG 1.4.1). */
.v-dot {
  display: inline-block;
  width: 8px;
  height: 8px;
  margin-left: 6px;
  border-radius: 50%;
  background: var(--v-red);
  vertical-align: middle;
}
