/* =========================================================================
 * Bastion Builder — Illuminated Codex (v1.1.0)
 *
 * Parchment + vermillion + gilt design language. Legacy token names
 * (--bg, --fg, --accent, …) remain as aliases over the Codex --bb-*
 * palette so unmodified rules in this file pick up the new theme via
 * the cascade. Per-surface restyles land in later slices.
 *
 * Spec: design/DESIGN_SPEC_codex.md
 * ========================================================================= */

/* =========================================================================
 * TABLE OF CONTENTS
 *
 * Generated -- run `python validate_styles.py --write` after adding or
 * renaming a section. No line numbers, deliberately: they would churn
 * on every edit and make each diff unreadable. Search the name.
 *
 *   Bastion Builder — Illuminated Codex (v1.1.0)
 *   Cascade layers (#259)
 *   Arcane Spellbook theme — token overrides (§5–§6)
 *   Arcane Spellbook theme — structural overrides (§2a–§2c)
 *   Cartographer's Chart theme — token overrides (§5)
 *   Verdant Keep theme — token overrides (§5)
 *   Verdant Keep theme — structural overrides
 *   Base elements + shared primitives
 *     - Scrollbars
 *     - Typography
 *     - Cards / panels
 *     - Form fields
 *     - Buttons
 *     - Two-column
 *     - Entry list (Special facilities, Basic rooms)
 *     - Picker
 *     - Picker panels (§5.6)
 *     - Kind-chooser: 2-up card grid (#169)
 *     - Picker · Responsive
 *     - Misc small bits
 *     - Summary bar
 *   FLOORPLAN
 *     - SVG overlays inside the floor canvas (Codex §6.5)
 *     - Placed facility rects (§6.5.1)
 *     - Walls / openings / doors (§6.5.5)
 *     - Corridor cells (§6.5.6)
 *     - Terrain paint (#143, blob contours + six kinds #236)
 *     - Vehicle environment wash (#238)
 *     - Defensive walls (§6.5.7)
 *     - Stairs (§6.5.9)
 *     - Annotations (§6.5.10)
 *     - Ghost rect + drag handles (§6.5.3)
 *   The app shell (#121)
 *     - App shell
 *     - Band 1: the controls-only top bar (#121, §5.2)
 *     - Band 2: chapter tabs (#121, §5.3)
 *     - Responsive: the tab band
 *     - Band 3: the page body (#121, §5.4)
 *     - Charter (chapter I, §5.4)
 *     - verso head: the arms in a cartouche
 *     - verso body: the alarm, then the roll
 *     - recto head: the plate
 *     - recto body: the story
 *     - the colophon: the whole survey, across the foot
 *     - Charter · Responsive
 *     - The Grounds (chapter II, §5.9)
 *     - The placeable-content library — right column (#161, §5.9)
 *     - Commission dialog (#121, §5.9)
 *     - The commissioning sheet (#218)
 *     - The docket
 *     - Bastions ▾ menu (Codex §4)
 *     - Open handout — top-bar primary CTA (§3.5)
 *     - Bastions ▾ hover-preview tooltip (#53 / §4.3)
 *     - Bastion name overlay (Charter, §5.4)
 *     - Heraldry crest slot (#123)
 *     - Cap meter (special facilities)
 *     - Cap meter (§5.4)
 *     - Facility icon discs (§5.5)
 *     - Info glyph + the defenses line (§5.2 / §5.3)
 *     - Right pane: the floorplan as hero
 *     - Stage container (Codex §6)
 *     - Placement banner — active/placing state (§6.1)
 *     - Grid controls (Codex §6.2)
 *     - Edit-mode toolbar (Codex §6.3)
 *     - Settings errand cards (Your library)
 *     - Settings two-column toggle block
 *     - Register of Chambers (#134)
 *     - Register card management (#121 Stage B)
 *     - Register codex: the index (#300)
 *     - Register codex: the leaf (#300)
 *     - Register codex: the front matter (#300)
 *     - Register codex: responsive (#300)
 *     - Dice roll tokens (#149)
 *     - SRD reference glyphs + stat cards (#213)
 *     - Name-suggestion popover (#208)
 *     - Register card detail
 *     - Plate-spine: single-active-floor switcher (#160, §5.9)
 *     - Active floor frame
 *     - Open handout dialog (#292)
 *     - Owner cards (#147)
 *     - Themed confirm + choice dialog (Codex §7.1)
 *     - Founding flow + welcome view
 *     - Milestones: lines, ids, footer
 *     - The Works frieze + phase records (#296)
 *     - Milestone details + crest gallery
 *     - The Household: hirelings + In Residence
 *     - Places open (#218)
 *     - Responsive: Places open
 *     - Chronicle › Stores + the facility roll (#296)
 *     - Responsive: Stores rows stack
 *     - Bastion Turn page (#112)
 *     - Responsive: the advance bar
 *   THE WRIT (#296)
 *     - Responsive: the roll and the writ at 390px
 *     - The done run + sealed-row record
 *     - Send verbs: VTT, Discord, share (#199/#209)
 *     - Turn trackers
 *     - The fore-edge index (#296)
 *     - The reckoning (#296)
 *     - Inline numeric + note editors
 *     - Help dialog + the plan band (#291)
 *     - Standalone Help page (#174)
 *     - Themed tooltip (§G)
 *   Facility hover tooltip (floor canvas)
 *   Credits footer — fixed bottom-right corner so it never reflows the
 *   Tablet / narrow-window tier (#218) — the top bar folds before it breaks.
 *   The Grounds' narrow tier (#275) — 701px to 1024px.
 *   Phone tier (#105) — the CROSS-SURFACE half
 *   Landscape-phone corrections (#283) — inside the tier, but WIDE.
 *     - D&D Beyond import dialog (#148)
 *   #209 — Shared-snapshot read-only viewer
 *   #303 — Sync-conflict band & concordance
 *     - the concordance, inside the choice modal
 *     - Responsive (phone tier)
 *   Homebrew (#146)
 *   Homebrew editor v2 (#226)
 *   HOME — the front door (#264)
 *     - Masthead
 *     - Bays
 *     - The roll
 *     - One entry
 *     - The "File under a campaign" dialog (#267)
 *     - The DM's Shelf (#255)
 *     - Key to the seals
 *     - Colophon
 *     - Responsive
 *     - The wire (#266): the opt-in, and the roster dialog
 *
 * PLACEMENT: a surface's rules live together, and its responsive rules
 * go LAST inside its own section -- after every base rule for it. That
 * ordering is load-bearing, not tidiness; validate_styles.py gate G6.
 * ========================================================================= */

/* Self-hosted Codex font families (#44). Files live under web/fonts/ as
 * woff2 — see web/fonts/SOURCES.md for the upstream Google Fonts URLs
 * and the CSP-allowance retirement this swap unlocks. Subsets cover
 * latin + latin-ext; anything outside falls back through the family
 * stack to Georgia / serif. */
@font-face {
  font-family: 'Cormorant Garamond';
  font-style: italic;
  font-weight: 400 500;
  font-display: swap;
  src: url(fonts/co3ZmX5slCNuHLi8bLeY9MK7whWMhyjYrEtGmSq17w.woff2) format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: 'Cormorant Garamond';
  font-style: italic;
  font-weight: 400 500;
  font-display: swap;
  src: url(fonts/co3ZmX5slCNuHLi8bLeY9MK7whWMhyjYrEtImSo.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Cormorant Garamond';
  font-style: normal;
  font-weight: 400 700;
  font-display: swap;
  src: url(fonts/co3bmX5slCNuHLi8bLeY9MK7whWMhyjYp3tKgS4.woff2) format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: 'Cormorant Garamond';
  font-style: normal;
  font-weight: 400 700;
  font-display: swap;
  src: url(fonts/co3bmX5slCNuHLi8bLeY9MK7whWMhyjYqXtK.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Cormorant SC';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url(fonts/0yb5GD4kxqXBmOVLG30OGwsupTAxxTY.woff2) format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: 'Cormorant SC';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url(fonts/0yb5GD4kxqXBmOVLG30OGwsuqzAx.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Cormorant SC';
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url(fonts/0ybmGD4kxqXBmOVLG30OGwsmWBMk9BfzwDM.woff2) format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: 'Cormorant SC';
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url(fonts/0ybmGD4kxqXBmOVLG30OGwsmWBMk-hfz.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Cormorant SC';
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url(fonts/0ybmGD4kxqXBmOVLG30OGwsmdBQk9BfzwDM.woff2) format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: 'Cormorant SC';
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url(fonts/0ybmGD4kxqXBmOVLG30OGwsmdBQk-hfz.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Cormorant SC';
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url(fonts/0ybmGD4kxqXBmOVLG30OGwsmEBUk9BfzwDM.woff2) format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: 'Cormorant SC';
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url(fonts/0ybmGD4kxqXBmOVLG30OGwsmEBUk-hfz.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'EB Garamond';
  font-style: italic;
  font-weight: 400 500;
  font-display: swap;
  src: url(fonts/SlGWmQSNjdsmc35JDF1K5GRweDU1ZzPJ.woff2) format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: 'EB Garamond';
  font-style: italic;
  font-weight: 400 500;
  font-display: swap;
  src: url(fonts/SlGWmQSNjdsmc35JDF1K5GRweDs1Zw.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'EB Garamond';
  font-style: normal;
  font-weight: 400 600;
  font-display: swap;
  src: url(fonts/SlGUmQSNjdsmc35JDF1K5GR7SDktYw.woff2) format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: 'EB Garamond';
  font-style: normal;
  font-weight: 400 600;
  font-display: swap;
  src: url(fonts/SlGUmQSNjdsmc35JDF1K5GR1SDk.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'IM Fell English';
  font-style: italic;
  font-weight: 400;
  font-display: swap;
  src: url(fonts/Ktk3ALSLW8zDe0rthJysWrnLsAzHFZOafYs.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'IM Fell English';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url(fonts/Ktk1ALSLW8zDe0rthJysWrnLsAzHEKOY.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'IM Fell English SC';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url(fonts/a8IENpD3CDX-4zrWfr1VY879qFF05pZ7PIIP.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Cinzel';
  font-style: normal;
  font-weight: 400 700;
  font-display: swap;
  src: url(fonts/8vIJ7ww63mVu7gt7-GT7LEc.woff2) format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: 'Cinzel';
  font-style: normal;
  font-weight: 400 700;
  font-display: swap;
  src: url(fonts/8vIJ7ww63mVu7gt79mT7.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* JetBrains Mono — self-hosted (#113): mono stack for Cartographer, Verdant, and Arcane.
 * Normal + italic, weight range 400–700 (variable font), latin + latin-ext only. */
@font-face {
  font-family: 'JetBrains Mono';
  font-style: normal;
  font-weight: 400 700;
  font-display: swap;
  src: url(fonts/tDbv2o-flEEny0FZhsfKu5WU4zr3E_BX0PnT8RD8yKwBNntkaToggR7BYRbKPx7cwhsk.woff2) format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: 'JetBrains Mono';
  font-style: normal;
  font-weight: 400 700;
  font-display: swap;
  src: url(fonts/tDbv2o-flEEny0FZhsfKu5WU4zr3E_BX0PnT8RD8yKwBNntkaToggR7BYRbKPxDcwg.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'JetBrains Mono';
  font-style: italic;
  font-weight: 400;
  font-display: swap;
  src: url(fonts/tDba2o-flEEny0FZhsfKu5WU4xD-IQ-PuZJJXxfpAO-LfmOZmaKjhw.woff2) format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: 'JetBrains Mono';
  font-style: italic;
  font-weight: 400;
  font-display: swap;
  src: url(fonts/tDba2o-flEEny0FZhsfKu5WU4xD-IQ-PuZJJXxfpAO-LfmOXmaI.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

/* =========================================================================
 * Cascade layers (#259)
 * =========================================================================
 * Later layers win over earlier ones regardless of
 * source order or selector specificity, so a rule's precedence is its layer,
 * not its position in this 10k-line file. This kills the "my override sits
 * above the base rule" bug family (e.g. base `.modal` clobbering
 * `.modal.overview-dialog` purely by coming later) and lets the compound-
 * specificity workarounds that dodged it relax.
 *
 *   reset      — element normalize / box-sizing
 *   base       — bare-element + single-primitive shell defaults (the rules a
 *                component is entitled to override): body, typography, the base
 *                `.modal` shell. NOT [data-theme] tokens — those stay in
 *                `components`; custom-property resolution is layer-independent.
 *   components — the bulk: shell, chapters, cards, forms, floorplan, and every
 *                class-specific rule incl. per-dialog overrides
 *   themes     — [data-theme] token + structural overrides (beat everything above)
 *   utilities  — helpers / responsive nudges (last word)
 *
 * There is deliberately no `dialogs` layer: the bug #259 kills is
 * defaults-clobber-overrides in general (modals, buttons, forms alike) — a
 * precedence-ROLE problem, not a dialog-FAMILY one. The fix is putting the
 * shell DEFAULT in `base`; per-dialog overrides just stay in `components` and
 * win by layer. A `dialogs` layer would name a component family inside a
 * precedence ladder — the wrong axis.
 *
 * @font-face above is intentionally left UNLAYERED — it defines font
 * resources, not cascade rules. */
@layer reset, base, components, themes, utilities;

@layer components {

:root {
  /* ---- Codex palette (§2.1) ---- */
  --bb-parchment:        #f1e5c6;
  --bb-parchment-light:  #f8efd4;
  --bb-parchment-mid:    #e8d9b3;
  --bb-parchment-dark:   #d9c592;
  --bb-parchment-deep:   #c7ad77;

  --bb-ink:              #2a1d0c;
  --bb-ink-soft:         #5a4423;
  --bb-ink-faint:        #6f5a32; /* darkened for AA — 5.3:1 on parchment, 4.71 on parchment-mid (#132/#218) */

  --bb-vermillion:       #a8281d;
  --bb-vermillion-deep:  #7a1b14;
  --bb-vermillion-light: #c64a3a;

  --bb-gilt:             #a87826;
  --bb-gilt-bright:      #d4a548;
  --bb-gilt-deep:        #7a5318;
  --bb-gilt-fill:        rgba(212,165,72,0.22);
  --bb-gilt-fill-strong: rgba(212,165,72,0.32);

  --bb-sapphire:         #2e4a7c;
  --bb-sapphire-deep:    #1f3458;
  --bb-sapphire-soft:    rgba(46,74,124,0.10);

  /* ---- Treatment tokens — promoted from hardcoded literals (#110) ---- */
  /* Codex values equal the prior hardcoded literals: zero visual change.  */
  /* Arcane values live in [data-theme="arcane"] (issue #55).              */

  /* facility fills */
  --bb-special-fill:         rgba(168,40,29,0.10);
  --bb-basic-fill:           rgba(46,74,124,0.10);
  --bb-special-fill-wash:    rgba(168,40,29,0.08);
  --bb-haunted:              #6b5a8a;
  --bb-haunted-wash:         rgba(107,90,138,0.08);
  --bb-basic-fill-stair:     rgba(46,74,124,0.20);
  --bb-stone-fill:           rgba(120,90,50,0.45);
  --bb-stone-fill-ghost:     rgba(120,90,50,0.36);
  --bb-stone-hatch:          rgba(60,40,15,0.30);
  --bb-drawbridge-fill:      rgba(196,158,102,0.94);
  --bb-corridor-fill:        rgba(80,40,10,0.18);
  /* Terrain paint (#143) — hue-for-material carve-out (§5.9): fill + contour
     line per kind. Ink-wash tints over the warm parchment. */
  --bb-terrain-water:        rgba(46,90,130,0.16);
  --bb-terrain-water-line:   rgba(34,68,104,0.60);
  --bb-terrain-road:         rgba(168,140,96,0.24);    /* packed earth — sand owns the pale dune (#236) */
  --bb-terrain-road-line:    rgba(128,102,52,0.55);
  --bb-terrain-grass:        rgba(126,150,74,0.15);
  --bb-terrain-grass-line:   rgba(92,116,48,0.55);
  --bb-terrain-sand:         rgba(222,200,132,0.20);
  --bb-terrain-sand-line:    rgba(176,150,86,0.55);
  --bb-terrain-rock:         rgba(120,116,108,0.18);
  --bb-terrain-rock-line:    rgba(78,74,66,0.58);
  --bb-terrain-marsh:        rgba(78,116,96,0.17);
  --bb-terrain-marsh-line:   rgba(46,80,64,0.60);
  /* Vehicle environment wash (#238) — FULL-BLEED behind a hull, so every fill
     sits lower in alpha than its --bb-terrain-* cousin. Sky is lighter and
     greyer-blue, water deeper and greener-blue: that fill-level separation
     survives even for a reader who never registers the marks. Ballast is
     grey-warm, never tan, so it can't be confused with the packed-earth road. */
  --bb-vehicle-water:        rgba(46,90,130,0.11);
  --bb-vehicle-water-deep:   rgba(34,68,104,0.18);
  --bb-vehicle-water-line:   rgba(34,68,104,0.50);
  --bb-vehicle-sky:          rgba(132,164,198,0.12);
  --bb-vehicle-sky-line:     rgba(96,124,158,0.42);
  --bb-vehicle-ballast:      rgba(122,112,98,0.14);
  --bb-vehicle-ballast-line: rgba(84,76,64,0.52);
  /* Conductor stones (#238) — held to a muted teal on parchment; a true Eberron
     cyan reads as a highlighter on this ground. */
  --bb-vehicle-conductor:      rgba(46,138,148,0.62);
  --bb-vehicle-conductor-glow: rgba(46,138,148,0.16);
  --bb-grid-line:            rgba(80,40,10,0.12);
  --bb-canvas-fleck:         rgba(60,40,15,0.04);

  /* shadow ramp */
  --bb-shadow:               rgba(80,40,10,0.35);
  --bb-shadow-mid:           rgba(80,40,10,0.40);
  --bb-shadow-deep:          rgba(80,40,10,0.45);
  --bb-shadow-inset-1:       rgba(80,40,10,0.10);
  --bb-shadow-inset-2:       rgba(80,40,10,0.12);

  /* inset highlights */
  --bb-inset-light:          rgba(255,250,235,0.50);
  --bb-inset-strong:         rgba(255,250,235,0.55);
  --bb-inset-faint:          rgba(255,250,235,0.15);

  /* accent soft washes */
  --bb-accent-soft:          rgba(168,40,29,0.15);
  --bb-accent-soft-hover:    rgba(168,40,29,0.22);

  /* tooltip rings */
  --bb-special-ring:         rgba(168,40,29,0.15);
  --bb-basic-ring:           rgba(46,74,124,0.15);

  /* structural / ambient */
  --bb-halo:                 var(--bb-parchment-light);
  --bb-hairline:             rgba(80,40,10,0.30);
  --bb-peek-wash:            rgba(80,40,10,0.08);
  --bb-below-wash:           rgba(80,40,10,0.06);
  --bb-vignette:             rgba(80,40,10,0.18);
  --bb-backdrop:             rgba(40,25,5,0.45);

  /* danger fill */
  --bb-danger-fill:          rgba(168,40,29,0.28);

  /* glow + muted accent */
  --bb-glow:                 transparent;
  --bb-accent-muted:         var(--bb-vermillion-deep);

  /* danger split */
  --bb-danger:               var(--bb-vermillion);
  --bb-danger-deep:          var(--bb-vermillion-deep);

  --bb-verdigris:        #4a6f3a;
  --bb-verdigris-deep:   #2f4a26;

  --bb-rule:             #8a6a35;
  --bb-rule-soft:        #b8985e;
  --bb-wall:             #3d2a14;

  /* ---- Codex type stack (§2.2) ---- */
  --bb-font-text:    'Cormorant Garamond', 'EB Garamond', Georgia, serif;
  --bb-font-display: 'IM Fell English SC', 'Cormorant SC', Georgia, serif;
  --bb-font-italic:  'IM Fell English', 'EB Garamond', Georgia, serif;
  --bb-font-mono:    ui-monospace, 'SF Mono', Consolas, monospace;
  /* Theme-INVARIANT sans for SVG map symbols (secret-door S, and future keyed
   * markers / compass letters / ruler labels). Deliberately NOT redefined per
   * theme — cartographic iconography must not re-theme — and web-safe so it
   * renders identically in the standalone SVG export. Prose SVG text (names,
   * annotations) keeps --bb-font-display. */
  --bb-font-map:     Arial, Helvetica, sans-serif;

  /* ---- Short aliases onto the --bb-* tokens. The per-slice restyle that was
   * going to retire these took most of them: what is left is the set rules
   * still read, so an unreferenced one is a leftover, not a slot to fill. ---- */
  --bg-card:     var(--bb-parchment-light);
  --bg-card-2:   var(--bb-parchment-mid);

  --fg:          var(--bb-ink);
  --fg-dim:      var(--bb-ink-soft);

  --accent:        var(--bb-vermillion);
  --danger:        var(--bb-danger);
  --success:       var(--bb-verdigris);

  --border:        var(--bb-rule);

  --font-display: var(--bb-font-display);
  --font-ui:      var(--bb-font-text);

  /* Squared corners across the chrome (§2.4). Per-surface exceptions
   * (progress-bar fill caps, pb-icon disc) re-introduce their own radius. */
  --radius:    0;
  --radius-sm: 0;
  --radius-lg: 0;

  /* Organic-corner token — Verdant overrides this for stat plaques (§7, DESIGN_SPEC_verdant.md).
   * All other themes keep it 0 so switching is a value-only swap. */
  --bb-radius-organic: 0;

  /* ---- Elevation — soft sepia drops on parchment (§2.4) ---- */
  --shadow-card:
    0 1px 0 var(--bb-parchment),
    0 18px 28px -22px var(--bb-shadow);
  --shadow-sunken:
    inset 0 1px 0 var(--bb-shadow-inset-1),
    inset 0 2px 4px var(--bb-shadow-inset-2);
  --shadow-pop:
    0 8px 18px -6px var(--bb-shadow),
    0 0 0 1px var(--bb-rule);
}

} /* @layer components — pause for the theme layer (#259) */

/* Theme overrides live in the `themes` layer so they beat every component and
   dialog rule regardless of source order. They already won today (token blocks
   by source order over base `:root`; structural blocks by [data-theme]
   specificity) — the layer makes that precedence intentional instead of
   incidental, and future-proofs it against a later same-specificity rule. */
@layer themes {

/* =========================================================================
 * Arcane Spellbook theme — token overrides (§5–§6)
 * ========================================================================= */
/* Arcane Spellbook theme — token overrides (§5–§6, DESIGN_SPEC_arcane.md)
 * Apply by setting data-theme="arcane" on <html> or <body>.
 * Every --bb-* token is overridden so the swap re-skins the
 * entire app with no DOM changes.
 * ============================================================ */
[data-theme="arcane"] {
  /* Ground — midnight indigo ramp (§5.1) */
  --bb-parchment:        #110e1d;
  --bb-parchment-light:  #211b34;
  --bb-parchment-mid:    #1a162a;
  --bb-parchment-dark:   #0c0a16;
  --bb-parchment-deep:   #080611;

  /* Ink — moonlight lavender (§5.2) */
  --bb-ink:        #e9e5f4;
  --bb-ink-soft:   #b2aac8;
  --bb-ink-faint:  #938bab; /* lightened for AA — ≥5.1:1 on arcane surfaces (#132) */

  /* Brand / active — arcane violet (§5.3) */
  --bb-vermillion:        #9a6df2;
  --bb-vermillion-deep:   #c2a6f7;  /* LUMINOUS — headings, wordmark, emphasis */
  --bb-vermillion-light:  #b48bf5;

  /* Secondary / progress / CTA — astral gold (§5.4) */
  --bb-gilt:         #c8a24f;
  --bb-gilt-bright:  #e6c87a;
  --bb-gilt-deep:    #a07e36;
  --bb-gilt-fill:    rgba(230,200,122,0.22);
  --bb-gilt-fill-strong: rgba(230,200,122,0.32);

  /* Basic-room ink — astral azure (§5.5) */
  --bb-sapphire:        #5a93d8;
  --bb-sapphire-deep:   #84b3e6;
  --bb-sapphire-soft:   rgba(90,147,216,0.14);

  /* Success — eldritch jade (§5.6) */
  --bb-verdigris:       #54b187;
  --bb-verdigris-deep:  #7ccaa0;

  /* Danger — crimson (§5.7 / §4 split) */
  --bb-danger:        #e2546b;
  --bb-danger-deep:   #ef7d8e;

  /* Hairlines + architecture (§5.8) */
  --bb-rule:       rgba(200,162,79,0.34);
  --bb-rule-soft:  rgba(176,166,210,0.22);
  --bb-wall:       #847d97;

  /* facility fills */
  --bb-special-fill:         rgba(154,109,242,0.16);
  --bb-basic-fill:           rgba(90,147,216,0.16);
  --bb-special-fill-wash:    rgba(154,109,242,0.10);
  --bb-haunted:              #8f86b8;
  --bb-haunted-wash:         rgba(143,134,184,0.12);
  --bb-basic-fill-stair:     rgba(90,147,216,0.24);
  --bb-stone-fill:           rgba(132,125,151,0.42);
  --bb-stone-fill-ghost:     rgba(132,125,151,0.36);
  --bb-stone-hatch:          rgba(132,125,151,0.55);
  --bb-drawbridge-fill:      rgba(178,150,112,0.92);
  --bb-corridor-fill:        rgba(176,166,210,0.12);
  /* Terrain (#143/#236): luminous re-hues, not re-alphas — low-alpha dark
     fills vanish on the midnight void. Azure water · packed-earth road ·
     jade grass/marsh · pale-gold sand · violet-grey rock (the arcane
     hard-ground family), with bright contour strokes. */
  --bb-terrain-water:        rgba(90,147,216,0.20);
  --bb-terrain-water-line:   rgba(132,179,230,0.60);
  --bb-terrain-road:         rgba(150,112,66,0.28);    /* deep packed earth — WARM by ruling: a violet road would collide with the lavender corridor wash */
  --bb-terrain-road-line:    rgba(206,170,112,0.58);   /* bright warm tan — the dim line here was a light-theme leftover (#236) */
  --bb-terrain-grass:        rgba(150,190,120,0.16);
  --bb-terrain-grass-line:   rgba(176,214,146,0.52);
  --bb-terrain-sand:         rgba(236,220,164,0.24);   /* luminous pale gold — re-HUED bright, not re-alphaed, to clear the earth road */
  --bb-terrain-sand-line:    rgba(234,218,164,0.48);
  --bb-terrain-rock:         rgba(150,146,168,0.18);   /* stays in the violet-grey family */
  --bb-terrain-rock-line:    rgba(188,184,206,0.52);
  --bb-terrain-marsh:        rgba(96,168,150,0.17);
  --bb-terrain-marsh-line:   rgba(140,200,182,0.55);
  /* Vehicle environment wash (#238) — chosen AGAINST the indigo ground, never
     inherited from the light themes (the #236 road-contour lesson). Two
     inversions are deliberate: below-the-waterline is DIMMER here, not darker
     (a darker tint on a dark ground is invisible), and the rail is the
     brightest mark on the canvas rather than the darkest. */
  --bb-vehicle-water:        rgba(96,150,214,0.16);    /* moonlit sea — lighter than the ground */
  --bb-vehicle-water-deep:   rgba(70,108,170,0.15);
  --bb-vehicle-water-line:   rgba(140,186,234,0.55);
  --bb-vehicle-sky:          rgba(150,150,206,0.13);   /* lavender night sky */
  --bb-vehicle-sky-line:     rgba(196,192,238,0.50);
  --bb-vehicle-ballast:      rgba(150,142,166,0.13);   /* violet-grey, kin to arcane rock */
  --bb-vehicle-ballast-line: rgba(190,184,206,0.48);
  /* Conductor stones (#238) — the one place this theme gets to be literal: a
     bright arcane cyan on the midnight field, exactly as the stones are drawn. */
  --bb-vehicle-conductor:      rgba(124,236,242,0.78);
  --bb-vehicle-conductor-glow: rgba(124,236,242,0.20);
  --bb-grid-line:            rgba(176,166,210,0.14);
  --bb-canvas-fleck:         rgba(206,196,255,0.07);

  /* shadow ramp */
  --bb-shadow:               rgba(0,0,0,0.55);
  --bb-shadow-mid:           rgba(0,0,0,0.65);
  --bb-shadow-deep:          rgba(0,0,0,0.72);
  --bb-shadow-inset-1:       rgba(0,0,0,0.18);
  --bb-shadow-inset-2:       rgba(0,0,0,0.22);

  /* inset highlights — faint lavender on dark */
  --bb-inset-light:          rgba(206,196,255,0.08);
  --bb-inset-strong:         rgba(206,196,255,0.10);
  --bb-inset-faint:          rgba(206,196,255,0.04);

  /* accent soft washes */
  --bb-accent-soft:          rgba(154,109,242,0.20);
  --bb-accent-soft-hover:    rgba(154,109,242,0.28);

  /* tooltip rings */
  --bb-special-ring:         rgba(154,109,242,0.20);
  --bb-basic-ring:           rgba(90,147,216,0.20);

  /* structural / ambient */
  --bb-halo:                 var(--bb-parchment);
  --bb-hairline:             rgba(176,166,210,0.22);
  --bb-peek-wash:            rgba(154,109,242,0.08);
  --bb-below-wash:           rgba(154,109,242,0.06);
  --bb-vignette:             rgba(0,0,5,0.45);
  --bb-backdrop:             rgba(4,3,10,0.62);

  /* danger fill */
  --bb-danger-fill:          rgba(226,84,107,0.28);

  /* glow + muted accent */
  --bb-glow:                 rgba(154,109,242,0.40);
  --bb-accent-muted:         #7a6490;

  /* Typography (§6) — body + italic carried by EB Garamond (§6.1):
   * IM Fell's letterpress ink-bleed reads as fringing on the dark ground. */
  --bb-font-text:    'EB Garamond', Georgia, serif;
  --bb-font-display: 'Cormorant SC', 'Cinzel', Georgia, serif;
  --bb-font-italic:  'EB Garamond', Georgia, serif;
  --bb-font-mono:    'JetBrains Mono', ui-monospace, Consolas, monospace;
}

/* =========================================================================
 * Arcane Spellbook theme — structural overrides (§2a–§2c)
 * ========================================================================= */
/* Arcane — structural overrides that can't be expressed as
 * token swaps alone (§2a–§2c, DESIGN_SPEC_arcane.md §8)
 * ============================================================ */

/* §2a — Body background: void + starfield */
[data-theme="arcane"] body {
  background-color: var(--bb-parchment);
  background-image:
    radial-gradient(ellipse at 25% 15%, rgba(154,109,242,0.07) 0%, transparent 55%),
    radial-gradient(ellipse at 75% 85%, rgba(90,147,216,0.05) 0%, transparent 50%),
    repeating-conic-gradient(var(--bb-canvas-fleck) 0% 25%, transparent 0% 50%) 0 0 / 34px 34px,
    repeating-conic-gradient(var(--bb-canvas-fleck) 0% 25%, transparent 0% 50%) 17px 17px / 46px 46px;
}

/* §2b — Banner pulse: violet glow aura at the 50% apex.
   @keyframes cannot be scoped inside a selector; declare a separate named
   animation and point the Arcane placement-banner at it.
   The Codex 50%-frame hardcodes rgba(212,165,72,0.55) (gilt); here it
   resolves through --bb-glow (violet) instead. */
@keyframes banner-pulse-arcane {
  0%, 100% {
    box-shadow:
      0 1px 0 var(--bb-inset-light) inset,
      0 6px 14px -10px var(--bb-hairline);
  }
  50% {
    box-shadow:
      0 1px 0 var(--bb-inset-light) inset,
      0 8px 22px -6px var(--bb-glow);
  }
}
[data-theme="arcane"] .placement-banner {
  animation-name: banner-pulse-arcane;
}

/* §2b — Luminous accent: glow moment #3 (§8)
   Applied only to the three sites: wordmark, bastion name drop cap,
   active mode pill. Nowhere else. */
[data-theme="arcane"] .top-bar-brand .wordmark {
  text-shadow: 0 0 18px var(--bb-glow);
}

[data-theme="arcane"] .charter-identity .bastion-name-display::first-letter,
[data-theme="arcane"] .house-name-display::first-letter {
  text-shadow: 0 0 14px var(--bb-glow);
}

[data-theme="arcane"] .grounds-command {
  background: linear-gradient(180deg,
      rgba(33,27,52,0.94),
      rgba(26,22,42,0.88));
}

/* Arcane: parchment-light flips to dark lavender under the token swap, so it
 * can't sit on gilt-deep at AA. Use plain --bb-gilt with warm-dark text —
 * mirrors the status chip + btn-primary Arcane treatment (~7.8:1). (#131) */
[data-theme="arcane"] .mode-btn.active,
[data-theme="arcane"] .mode-btn[aria-pressed="true"] {
  background: var(--bb-gilt);
  color: #2a2008;
  border-color: var(--bb-gilt);
  box-shadow:
    0 1px 0 var(--bb-inset-faint) inset,
    0 0 10px var(--bb-glow);
}

/* §2c — CTA button: gilt fill keeps dark text in Arcane.
   btn-primary uses color: var(--bb-ink) which becomes lavender (#e9e5f4)
   under the Arcane token swap. Override to warm dark so gold stays legible. */
[data-theme="arcane"] button.btn-primary {
  color: #2a2008;
}

/* Top-bar: replace hardcoded warm parchment gradient with midnight indigo. */
[data-theme="arcane"] .top-bar {
  background: linear-gradient(180deg,
      rgba(33,27,52,0.88) 0%,
      rgba(26,22,42,0.70) 100%);
}

/* Stage: replace warm parchment radial vignette with a faint violet glow. */
[data-theme="arcane"] .stage {
  background: radial-gradient(ellipse 60% 40% at 50% 0%,
      rgba(154,109,242,0.08) 0%,
      transparent 60%);
}

/* Placement banner: background uses hardcoded gilt rgba; swap to arcane violet. */
[data-theme="arcane"] .placement-banner {
  background: linear-gradient(180deg,
      rgba(154,109,242,0.14),
      rgba(154,109,242,0.08));
}

/* Tool-mode chip in Arcane: parchment-light flips to dark lavender under the
 * Arcane token swap, so it can't sit on gilt-deep at AA. Use plain --bb-gilt
 * with warm-dark text — mirrors the btn-primary Arcane treatment above
 * (~7.8:1). (#131) */
[data-theme="arcane"] .placement-banner.mode #placement-status {
  background: var(--bb-gilt);
  color: #2a2008;
}

/* Cap meter fill: dial back the gradient peak so it reads as inlaid gold, not emission. */
[data-theme="arcane"] .cap-row .meter > .fill {
  background: linear-gradient(90deg, var(--bb-gilt-deep), var(--bb-gilt));
}

/* =========================================================================
 * Cartographer's Chart theme — token overrides (§5)
 * ========================================================================= */
/* Cartographer's Chart — token overrides (§5, DESIGN_SPEC_cartographer.md)
 * Apply by setting data-theme="cartographer" on <html>.
 * Light theme: temperature inversion — cool Admiralty-blue brand,
 * warm iron-gall sepia basic foil, no ground ramp flip.
 * ============================================================ */
[data-theme="cartographer"] {
  /* Ground — cool sun-bleached linen (§5.1) */
  --bb-parchment:        #e7e2d3;
  --bb-parchment-light:  #f1ede1;
  --bb-parchment-mid:    #ddd6c2;
  --bb-parchment-dark:   #cfc6ad;
  --bb-parchment-deep:   #bdb293;

  /* Ink — chart brown (§5.2) */
  --bb-ink:              #2a2419;
  --bb-ink-soft:         #5c5142;
  --bb-ink-faint:        #665b45; /* 4.60:1 on parchment-MID — the sunken card ground (#218) */

  /* Brand / active — Admiralty blue (§5.3) */
  --bb-vermillion:        #1f4e6b;
  --bb-vermillion-deep:   #163a51; /* DARKER — light theme emphasis; unlike Arcane's luminous flip */
  --bb-vermillion-light:  #2f6f93;

  /* Secondary / CTA — compass-rose brass (§5.4) */
  --bb-gilt:             #b08b3e;
  --bb-gilt-bright:      #d4ad5a;
  /* Darkened from #8a6a2a in #301. gilt-deep is the SMALL-TEXT gilt — the
   * circulation verbs, the jump links — and at 13px on --bb-parchment it
   * measured 3.88:1 here, under the 4.5:1 AA bar, in a theme whose ground is
   * lighter than codex's. 4.60:1 now, 5.27:1 on --bb-parchment-light. Same hue,
   * 90% value; the bright/base gilts are untouched because neither carries
   * small text. */
  --bb-gilt-deep:        #7c5f26;
  --bb-gilt-fill:        rgba(212,173,90,0.22);
  --bb-gilt-fill-strong: rgba(212,173,90,0.32);

  /* Basic-room ink — iron-gall sepia (§5.5)
   * NOTE: --bb-sapphire resolves to a WARM value here — intentional role-vs-name.
   * The role is "contrasting-temperature info ink"; Cartographer inverts the
   * temperature so the foil is warm sepia. Do not "fix" this toward blue. */
  --bb-sapphire:         #7a4a24;
  --bb-sapphire-deep:    #5e3618;
  --bb-sapphire-soft:    rgba(122,74,36,0.12);

  /* Success — sea-green (§5.6) */
  --bb-verdigris:        #3f7a5a;
  --bb-verdigris-deep:   #2c5640;

  /* Danger — madder red (§5.7) */
  --bb-danger:           #a63a2a;
  --bb-danger-deep:      #7e2a1d;

  /* Hairlines + architecture (§5.8) */
  --bb-rule:             #8a7048;
  --bb-rule-soft:        #b3a079;
  --bb-wall:             #163a51; /* deep Admiralty — chart structural ink; wall≠brand by weight */

  /* facility fills (§3.1) */
  --bb-special-fill:         rgba(31,78,107,0.10);
  --bb-basic-fill:           rgba(122,74,36,0.10);   /* warm sepia — the temperature inversion */
  --bb-special-fill-wash:    rgba(31,78,107,0.08);
  --bb-haunted:              #5f5780;
  --bb-haunted-wash:         rgba(95,87,128,0.08);
  --bb-basic-fill-stair:     rgba(122,74,36,0.20);
  --bb-stone-fill:           rgba(110,90,60,0.42);
  --bb-stone-fill-ghost:     rgba(110,90,60,0.36);
  --bb-stone-hatch:          rgba(60,45,25,0.30);
  --bb-drawbridge-fill:      rgba(190,158,108,0.93);
  --bb-corridor-fill:        rgba(31,78,107,0.12);   /* charted water-channel wash */
  /* Terrain (#143/#236): water bumps the charted-wash blue to 0.16 so it
     out-reads the corridor; drafted-earth road, muted sepia-leaning greens
     and greys for the soft grounds. */
  --bb-terrain-water:        rgba(31,78,107,0.16);
  --bb-terrain-water-line:   rgba(22,58,81,0.60);
  --bb-terrain-road:         rgba(158,130,88,0.24);   /* packed earth vs the charted-wash corridor — sand owns the dune (#236) */
  --bb-terrain-road-line:    rgba(120,92,44,0.55);
  --bb-terrain-grass:        rgba(120,138,72,0.15);
  --bb-terrain-grass-line:   rgba(84,104,44,0.55);
  --bb-terrain-sand:         rgba(210,186,124,0.20);
  --bb-terrain-sand-line:    rgba(166,138,80,0.55);
  --bb-terrain-rock:         rgba(112,106,96,0.18);
  --bb-terrain-rock-line:    rgba(72,66,56,0.58);
  --bb-terrain-marsh:        rgba(70,106,86,0.17);
  --bb-terrain-marsh-line:   rgba(42,74,58,0.60);
  /* Vehicle environment wash (#238) — sits in the chart's own ink family, so a
     ship under Admiralty ruling reads as a vessel plotted on the sheet. */
  --bb-vehicle-water:        rgba(31,78,107,0.12);
  --bb-vehicle-water-deep:   rgba(22,58,81,0.19);
  --bb-vehicle-water-line:   rgba(22,58,81,0.52);
  --bb-vehicle-sky:          rgba(108,144,176,0.12);
  --bb-vehicle-sky-line:     rgba(70,102,134,0.44);
  --bb-vehicle-ballast:      rgba(116,108,96,0.15);
  --bb-vehicle-ballast-line: rgba(76,70,58,0.54);
  --bb-vehicle-conductor:      rgba(30,120,134,0.62);
  --bb-vehicle-conductor-glow: rgba(30,120,134,0.15);
  --bb-grid-line:            rgba(31,78,107,0.16);   /* Admiralty chart ruling */
  --bb-canvas-fleck:         rgba(31,78,107,0.05);   /* linen stipple / rhumb wash base */

  /* shadow ramp — light theme, warm sepia family (§3.1) */
  --bb-shadow:               rgba(70,55,35,0.35);
  --bb-shadow-mid:           rgba(70,55,35,0.40);
  --bb-shadow-deep:          rgba(70,55,35,0.45);
  --bb-shadow-inset-1:       rgba(70,55,35,0.10);
  --bb-shadow-inset-2:       rgba(70,55,35,0.12);

  /* inset highlights */
  --bb-inset-light:          rgba(255,253,246,0.50);
  --bb-inset-strong:         rgba(255,253,246,0.55);
  --bb-inset-faint:          rgba(255,253,246,0.15);

  /* accent soft washes */
  --bb-accent-soft:          rgba(31,78,107,0.15);
  --bb-accent-soft-hover:    rgba(31,78,107,0.22);

  /* tooltip rings */
  --bb-special-ring:         rgba(31,78,107,0.15);
  --bb-basic-ring:           rgba(122,74,36,0.15);

  /* structural / ambient */
  --bb-halo:                 var(--bb-parchment-light);
  --bb-hairline:             rgba(31,78,107,0.30);
  --bb-peek-wash:            rgba(31,78,107,0.08);
  --bb-below-wash:           rgba(31,78,107,0.06);
  --bb-vignette:             rgba(70,55,35,0.16);
  --bb-backdrop:             rgba(40,30,15,0.45);

  /* danger fill */
  --bb-danger-fill:          rgba(166,58,42,0.28);

  /* glow + muted accent — NO glow; Cartographer's signature is precision linework */
  --bb-glow:                 transparent;
  --bb-accent-muted:         #6b7480;

  /* Typography (§6) — body + italic carried by EB Garamond (§6.1):
   * IM Fell's antique bleed is off-voice for chart precision linework. */
  --bb-font-text:    'EB Garamond', Georgia, serif;
  --bb-font-display: 'Cinzel', 'Cormorant SC', Georgia, serif;
  --bb-font-italic:  'EB Garamond', Georgia, serif;
  --bb-font-mono:    'JetBrains Mono', ui-monospace, Consolas, monospace;
}

/* Cartographer — CTA button keeps dark text on brass fill */
[data-theme="cartographer"] button.btn-primary {
  color: #2a2008;
}

/* =========================================================================
 * Verdant Keep theme — token overrides (§5)
 * ========================================================================= */
/* Verdant Keep — token overrides (§5, DESIGN_SPEC_verdant.md)
 * Apply by setting data-theme="verdant" on <html>.
 * Mid-light theme: sage-stone ground, moss brand, verdigris-copper
 * patina basic foil, spring-green success (§4 three-green resolution).
 * ============================================================ */
[data-theme="verdant"] {
  /* Ground — sage-stone (§5.1) — brand-new hue, real token stress test */
  --bb-parchment:        #d6dcc4;
  --bb-parchment-light:  #e6e9d6;
  --bb-parchment-mid:    #c8d0b2;
  --bb-parchment-dark:   #b6c09c;
  --bb-parchment-deep:   #a3ae86;

  /* Ink — bark brown (§5.2) */
  --bb-ink:              #33291c;
  --bb-ink-soft:         #5e503a;
  --bb-ink-faint:        #61553c; /* 4.57:1 on parchment-MID — the sunken card ground (#218) */

  /* Brand / active — moss green (§5.3) */
  --bb-vermillion:        #3f5e30;
  --bb-vermillion-deep:   #2c4420; /* DARKER — light-family emphasis */
  --bb-vermillion-light:  #587b44;

  /* Secondary / CTA — gold leaf (§5.4) */
  --bb-gilt:             #a8862e;
  --bb-gilt-bright:      #cda742;
  /* Darkened from #836726 in #301 — see the cartographer note. 3.79:1 on
   * --bb-parchment was the worst of the four themes; 4.59:1 now, 5.28:1 on
   * --bb-parchment-light. */
  --bb-gilt-deep:        #735b21;
  --bb-gilt-fill:        rgba(205,167,66,0.22);
  --bb-gilt-fill-strong: rgba(205,167,66,0.32);

  /* Basic-room ink — verdigris-copper patina (§5.5)
   * NOTE: --bb-sapphire role = "cool contrasting info ink" — here it's cool teal
   * patina, the temperature foil to warm moss. Do not "fix" toward lapis blue.
   * Ink tokens darkened for AA contrast; wash tokens keep the brighter mockup
   * patina (§5.5 ink-vs-wash split — do not consistency-fix fills to match ink). */
  --bb-sapphire:         #336757;
  --bb-sapphire-deep:    #27554a;
  --bb-sapphire-soft:    rgba(74,140,122,0.13); /* wash: brighter mockup value — correct */

  /* Success — spring green (§5.6)
   * §4 three-green resolution: brighter + more saturated than moss brand and patina info.
   * Luminance: spring 0.26 vs moss 0.09 / patina 0.11 — separates by value in greyscale. */
  --bb-verdigris:        #5e9a35;
  --bb-verdigris-deep:   #436e25;

  /* Danger — rust (§4.1) */
  --bb-danger:           #a4502a;
  --bb-danger-deep:      #7c3a1d;

  /* Hairlines + architecture (§5.8) */
  --bb-rule:             #7d7a4a;
  --bb-rule-soft:        #a7a878;
  --bb-wall:             #3a3a28; /* dark lichened stone */

  /* facility fills (§3.1) */
  --bb-special-fill:         rgba(63,94,48,0.12);
  --bb-basic-fill:           rgba(74,140,122,0.13);  /* brighter patina wash — §5.5 ink-vs-wash split */
  --bb-special-fill-wash:    rgba(63,94,48,0.08);
  --bb-haunted:              #61567f;
  --bb-haunted-wash:         rgba(97,86,127,0.08);
  --bb-basic-fill-stair:     rgba(74,140,122,0.24);
  --bb-stone-fill:           rgba(110,100,70,0.44);
  --bb-stone-fill-ghost:     rgba(110,100,70,0.36);
  --bb-stone-hatch:          rgba(60,55,30,0.32);
  --bb-drawbridge-fill:      rgba(188,162,112,0.93);
  --bb-corridor-fill:        rgba(94,80,58,0.20);    /* earthen trodden path — warm brown, distinct from moss/patina */
  /* Terrain (#143/#236) on a green ground: water pushed bluer (away from
     teal patina); road a light packed earth (must not read as the darker
     earthen corridor); grass leans hard YELLOW and marsh hard TEAL so the
     two terrain greens separate from moss brand + spring success greens —
     the four-green resolution, held with forest retired. */
  --bb-terrain-water:        rgba(58,110,140,0.18);
  --bb-terrain-water-line:   rgba(38,84,110,0.60);
  --bb-terrain-road:         rgba(166,140,92,0.26);   /* packed earth — sand owns the pale dune (#236) */
  --bb-terrain-road-line:    rgba(120,98,54,0.55);
  --bb-terrain-grass:        rgba(110,146,68,0.16);   /* leans hard yellow — clear of water/marsh in the green field */
  --bb-terrain-grass-line:   rgba(70,104,40,0.58);
  --bb-terrain-sand:         rgba(216,192,128,0.22);
  --bb-terrain-sand-line:    rgba(172,144,84,0.55);
  --bb-terrain-rock:         rgba(116,112,102,0.19);
  --bb-terrain-rock-line:    rgba(74,70,60,0.60);
  --bb-terrain-marsh:        rgba(64,100,78,0.18);   /* leans hard teal */
  --bb-terrain-marsh-line:   rgba(36,68,52,0.62);
  /* Vehicle environment wash (#238) — this ground already reads blue-green, so
     the fills are held to the low end or the sea swallows the whole sheet. */
  --bb-vehicle-water:        rgba(58,110,140,0.12);
  --bb-vehicle-water-deep:   rgba(38,84,110,0.20);
  --bb-vehicle-water-line:   rgba(38,84,110,0.52);
  --bb-vehicle-sky:          rgba(120,152,180,0.13);
  --bb-vehicle-sky-line:     rgba(74,106,136,0.46);
  --bb-vehicle-ballast:      rgba(118,112,100,0.16);
  --bb-vehicle-ballast-line: rgba(72,68,58,0.56);
  /* Pushed cooler and lighter than the terrain marsh teal so the stones can't
     be mistaken for wet ground on this green-leaning field. */
  --bb-vehicle-conductor:      rgba(38,146,158,0.64);
  --bb-vehicle-conductor-glow: rgba(38,146,158,0.17);
  --bb-grid-line:            rgba(63,94,48,0.18);    /* moss ruling */
  --bb-canvas-fleck:         rgba(63,94,48,0.05);    /* mossy stipple */

  /* shadow ramp — mid-light theme, warm-green family (§3.1) */
  --bb-shadow:               rgba(45,52,28,0.38);
  --bb-shadow-mid:           rgba(45,52,28,0.44);
  --bb-shadow-deep:          rgba(45,52,28,0.50);
  --bb-shadow-inset-1:       rgba(45,52,28,0.12);
  --bb-shadow-inset-2:       rgba(45,52,28,0.14);

  /* inset highlights */
  --bb-inset-light:          rgba(247,250,238,0.50);
  --bb-inset-strong:         rgba(247,250,238,0.55);
  --bb-inset-faint:          rgba(247,250,238,0.15);

  /* accent soft washes */
  --bb-accent-soft:          rgba(63,94,48,0.16);
  --bb-accent-soft-hover:    rgba(63,94,48,0.24);

  /* tooltip rings */
  --bb-special-ring:         rgba(63,94,48,0.16);
  --bb-basic-ring:           rgba(74,140,122,0.16);

  /* structural / ambient */
  --bb-halo:                 var(--bb-parchment-light);
  --bb-hairline:             rgba(63,94,48,0.30);
  --bb-peek-wash:            rgba(63,94,48,0.08);
  --bb-below-wash:           rgba(63,94,48,0.06);
  --bb-vignette:             rgba(45,52,28,0.20);
  --bb-backdrop:             rgba(25,30,15,0.48);

  /* danger fill */
  --bb-danger-fill:          rgba(164,80,42,0.30);

  /* glow + muted accent — NO glow; Verdant's signature is living growth */
  --bb-glow:                 transparent;
  --bb-accent-muted:         #6a7458;

  /* Organic corner radius for stat plaques (§7, decision #2) */
  --bb-radius-organic:       2px 10px 2px 10px;

  /* Typography (§6, rev 3) — display reversed to Cormorant Garamond (the
   * mockup's softer voice; decision #3 revisited): with Arcane already on
   * Cormorant SC, the SC choice left the two themes typographically
   * identical. Not a small-caps face, so tracked-caps labels get a
   * weight floor below (§6.1 rev 3). Body + italic carried by EB Garamond:
   * IM Fell's ink-bleed reads as grime on the sage ground (§6.1). */
  --bb-font-text:    'EB Garamond', Georgia, serif;
  --bb-font-display: 'Cormorant Garamond', 'Cormorant SC', Georgia, serif;
  --bb-font-italic:  'EB Garamond', Georgia, serif;
  --bb-font-mono:    'JetBrains Mono', ui-monospace, Consolas, monospace;
}

/* =========================================================================
 * Verdant Keep theme — structural overrides
 * ========================================================================= */
/* Verdant Keep — structural overrides (DESIGN_SPEC_verdant.md)
 * Two overrides: the tracked-caps weight floor (§6.1 rev 3) and the
 * organic glyph-well shape (§7): leaf corner vs. the default disc.
 * All other elements stay squared — canvas, rects, modals, mode buttons.
 * ============================================================ */

/* CTA button keeps dark text on gold-leaf fill */
[data-theme="verdant"] button.btn-primary {
  color: #2c2206;
}

/* Display-face weight floor (§6.1 rev 3): Cormorant Garamond is not a
 * small-caps face — at the 10–14px tracked-caps label sizes its 400 weight
 * reads anemic where the SC/engraved faces of the other themes hold the
 * line. Floor every --bb-font-display consumer ≤14px UPPERCASE that does
 * not already set 600+. Keep in sync when adding tracked-caps labels. */
[data-theme="verdant"] .top-bar .btn-ghost,
[data-theme="verdant"] .btn-handout,
[data-theme="verdant"] .cap-row .label-row,
[data-theme="verdant"] .defenses-line .defenses-label,
[data-theme="verdant"] .defenses-badge,
[data-theme="verdant"] .placement-banner #btn-cancel-placement,
[data-theme="verdant"] .grid-ctrls label,
[data-theme="verdant"] .grid-ctrls > button,
[data-theme="verdant"] .grid-ctrls .map-display-wrap > button,
[data-theme="verdant"] .mode-btn,
[data-theme="verdant"] .plate-eyebrow,
[data-theme="verdant"] .plate-label,
[data-theme="verdant"] .spine-add-btn,
[data-theme="verdant"] .annotation-popover-actions button,
[data-theme="verdant"] .dialog-actions .dialog-cancel,
[data-theme="verdant"] .dialog-actions .dialog-confirm,
[data-theme="verdant"] .dialog-actions-list .dialog-choice-label,
[data-theme="verdant"] .milestone-highlight-btn,
[data-theme="verdant"] .milestone-action,
[data-theme="verdant"] .help-nav a,
[data-theme="verdant"] #facility-tooltip .tt-meta {
  font-weight: 600;
}

} /* @layer themes */

/* =========================================================================
 * Base elements + shared primitives
 * =========================================================================
 * The reset, the bare-element defaults, and the class-level primitives every
 * surface reuses: typography, cards, form fields, buttons, pickers. Anything
 * here is something a component is entitled to override.
 * ========================================================================= */

@layer reset {
* { box-sizing: border-box; }
}

/* Bare-element defaults live in `base` so any specific component rule beats
   them by layer, not by being authored later (#259). These already lost to
   later component rules by source order today — the layer makes that
   intentional. Class-specific primitives (.card, button.btn-primary, …) stay
   in `components`; they are the specific rules, not the defaults. */
@layer base {

body {
  margin: 0;
  font-family: var(--bb-font-text);
  font-weight: 400;
  font-variant-numeric: lining-nums tabular-nums;
  color: var(--bb-ink);
  padding: 20px;
  line-height: 1.45;
  min-height: 100vh;

  /* Parchment ground — copied verbatim from Illuminated Codex.html (§2.1). */
  background:
    radial-gradient(ellipse at 20% 10%, rgba(255,245,210,0.7), transparent 60%),
    radial-gradient(ellipse at 80% 90%, rgba(140,90,30,0.16), transparent 60%),
    radial-gradient(ellipse at 60% 50%, rgba(180,130,60,0.06), transparent 50%),
    repeating-linear-gradient(0deg, rgba(120,80,30,0.035) 0 1px, transparent 1px 32px),
    var(--bb-parchment);
  background-attachment: fixed;
}

/* ---- Scrollbars --------------------------------------------------------- */
/* W3C spec (Firefox; Chrome 121+): parchment track, ink thumb.
 * All four themes inherit correct values automatically via --bb-* tokens. */
* {
  scrollbar-width: thin;
  scrollbar-color: var(--bb-ink-soft) var(--bb-parchment-dark);
}

/* Chrome / Edge / Safari: ::-webkit-scrollbar pseudo-elements */
::-webkit-scrollbar              { width: 8px; height: 8px; }
::-webkit-scrollbar-track        { background: var(--bb-parchment-dark); }
::-webkit-scrollbar-thumb        { background: var(--bb-ink-soft); }
::-webkit-scrollbar-thumb:hover  { background: var(--bb-ink); }
::-webkit-scrollbar-corner       { background: var(--bb-parchment-dark); }

/* Page-edge vignette (§2.1). z-index over chrome but below modals. */
body::before {
  content: '';
  position: fixed;
  inset: 0;
  pointer-events: none;
  background: radial-gradient(ellipse at center, transparent 50%, var(--bb-vignette) 100%);
  z-index: 100;
}

/* ---- Typography --------------------------------------------------------- */

h1, h2, h3 {
  margin: 0 0 10px 0;
  font-family: var(--font-display);
  font-weight: 600;
  letter-spacing: -0.005em;
  color: var(--fg);
}
h1 {
  font-size: 1.95rem;
  line-height: 1.05;
  color: var(--accent);            /* brand mark — the one place gold leads */
  font-weight: 700;
  letter-spacing: -0.015em;
}
h2 {
  font-size: 1.35rem;
  line-height: 1.1;
  color: var(--fg);                /* demoted from gold */
}
h3 {
  font-size: 0.84rem;
  font-family: var(--font-ui);
  font-weight: 600;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--fg-dim);
  margin-top: 14px;
}

} /* @layer base */

@layer components {

/* Tabular figures everywhere a number appears in a stat or input */
.summary strong,
input[type="number"] {
  font-variant-numeric: tabular-nums;
  font-feature-settings: "tnum", "ss01";
}

/* ---- Cards / panels ----------------------------------------------------- */

.card {
  background:
    linear-gradient(180deg, var(--bg-card) 0%, var(--bg-card-2) 100%);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  padding: 16px 18px;
  margin-bottom: 14px;
  box-shadow: var(--shadow-card);
}

/* ---- Form fields -------------------------------------------------------- */

input, select {
  background: var(--bb-parchment-light);
  color: var(--bb-ink);
  border: 1px solid var(--bb-rule);
  border-radius: 0;
  padding: 6px 9px;
  font: inherit;
  font-family: var(--bb-font-text);
  min-width: 120px;
  box-shadow: var(--shadow-sunken);
  transition: border-color 0.12s, box-shadow 0.12s;
}
/* The dropdown LIST is drawn by the OS and does not inherit the select's font,
   so a styled control opened into a system-font list — two typefaces in one
   act, reading as two different applications. Chromium and Firefox honour
   font-family on the option itself; Safari ignores it and degrades to exactly
   today's behaviour, so there is nothing to guard. Only the family is set:
   the popup's colours come from the OS, and overriding half of that pairing
   is how you end up with unreadable text in somebody's theme. */
option { font-family: var(--bb-font-text); }
input[type="number"] {
  max-width: 80px;
  appearance: textfield;        /* Firefox: hide native spin buttons */
}
input[type="number"]::-webkit-inner-spin-button,
input[type="number"]::-webkit-outer-spin-button {
  appearance: none;             /* WebKit/Blink: hide native spin buttons */
}
input[type="checkbox"] {
  min-width: 0;
  width: auto;
  padding: 0;
  margin: 0;
  border: none;
  background: none;
  box-shadow: none;
}
input:focus, select:focus, textarea:focus {
  outline: none;
  border-color: var(--bb-vermillion);
  box-shadow: var(--shadow-sunken), 0 0 0 2px rgba(168,40,29,0.20);
}

/* ---- Buttons ------------------------------------------------------------ */

button {
  background: linear-gradient(180deg, var(--bb-parchment-light), var(--bb-parchment-mid));
  color: var(--bb-ink);
  border: 1px solid var(--bb-rule);
  border-radius: 0;
  padding: 6px 12px;
  font: inherit;
  font-family: var(--bb-font-text);
  font-weight: 500;
  cursor: pointer;
  box-shadow:
    0 1px 0 var(--bb-inset-light) inset,
    0 1px 2px rgba(80,40,10,0.15);
  transition: background 0.12s, border-color 0.12s, transform 0.04s;
}
button:hover {
  background: linear-gradient(180deg, var(--bb-parchment-light), var(--bb-parchment-light));
  border-color: var(--bb-ink-faint);
}
button:active { transform: translateY(1px); }
button:disabled {
  opacity: 0.45;
  cursor: not-allowed;
  box-shadow: none;
}
button:disabled:hover {
  background: linear-gradient(180deg, var(--bb-parchment-light), var(--bb-parchment-mid));
  border-color: var(--bb-rule);
}

/* Primary (gilt-filled) — handout CTA + modal confirm. Slice 7 narrows
 * this further on the modal Save/Confirm buttons. */
button.btn-primary {
  background: linear-gradient(180deg, var(--bb-gilt-bright), var(--bb-gilt));
  color: var(--bb-ink);
  border: 1px solid var(--bb-gilt-deep);
  font-weight: 600;
  box-shadow:
    0 1px 0 var(--bb-inset-strong) inset,
    0 2px 4px var(--bb-shadow);
}
button.btn-primary:hover {
  background: linear-gradient(180deg,
      color-mix(in oklch, var(--bb-gilt-bright), white 6%),
      var(--bb-gilt-bright));
  border-color: var(--bb-gilt-deep);
}

/* Danger — vermillion outline on parchment, fill on hover. */
button.btn-danger,
button.danger {
  border-color: var(--bb-vermillion);
  color: var(--bb-vermillion);
  background: transparent;
  box-shadow: none;
}
button.btn-danger:hover,
button.danger:hover {
  background: var(--bb-vermillion);
  color: var(--bb-parchment-light);
  border-color: var(--bb-vermillion);
}

@media (pointer: coarse) {
  button, select, input[type="number"] {
    min-height: 44px;
    padding-top: 9px;
    padding-bottom: 9px;
  }
  /* Inline tap targets (#158): help dots, the defenses badge, dice tokens,
   * and the result-row delete crosses sit inside headings, stat lines,
   * justified prose, or a 1.25rem grid track. The 44px button rule above
   * doesn't reach them (spans / inline boxes), and padding would inflate
   * their bordered visual boxes — so grow only the *hit area* with an
   * invisible ::after overlay. .info-i and the badge already open Help on
   * tap via their data-help-section click delegate (app.js); this just makes
   * that reachable by thumb.
   *
   * .grid-dims joined the list when it became a Help door (#291). Note this
   * block is (pointer: coarse), NOT the phone tier — the readout is hidden on
   * phones, so the tier that needs the halo is a coarse tablet ≥701px, where
   * it renders as ~15px of 13px mono. */
  .info-i,
  .defenses-badge,
  .grid-dims,
  .roll-die,
  .roll-die-chip,
  .ref-glyph,
  .turn-result-del,
  .turn-row-del {
    position: relative;
  }
  /* .ref-glyph (#213) was missed off this list, and being a real <button> the
     44px rule above caught it: a 0.85em glyph inflated into a bordered ~12×44
     sliver. Worse, the tall inline box detaches the versal `::first-letter`
     beside it, so a recruit card rendered a lone drop-cap with the rest of the
     name dropped to the next line. Undo the inflation, then grow the hit area
     the way every other inline tap target here does. */
  .ref-glyph {
    min-height: 0;
    padding-top: 0;
    padding-bottom: 0;
  }
  .info-i::after,
  .defenses-badge::after,
  .grid-dims::after,
  .roll-die::after,
  .roll-die-chip::after,
  .ref-glyph::after {
    content: "";
    position: absolute;
    inset: -12px;
  }
  /* The delete crosses are real buttons (44px tall via the rule above) in a
   * 1.25rem grid track — widen sideways only, so the halo never steals taps
   * from the row above/below. */
  .turn-result-del::after,
  .turn-row-del::after {
    content: "";
    position: absolute;
    inset: 0 -12px;
  }
}

/* ---- Two-column --------------------------------------------------------- */

/* ---- Entry list (Special facilities, Basic rooms) ----------------------- */

.picker-list {
  list-style: none;
  margin: 0;
  padding: 0;
}

/* ---- Picker ------------------------------------------------------------- */

/* ---- Picker panels (§5.6) ----------------------------------------------- */

.picker {
  margin-top: 12px;
  padding: 12px;
  border: 1px dashed var(--bb-rule-soft);
  border-radius: 0;
  background: var(--bb-parchment-mid);
  box-shadow: var(--shadow-sunken);
}
.picker-list li {
  padding: 8px 10px;
  border-radius: 0;
  cursor: pointer;
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 10px;
  transition: background 0.1s;
}
.picker-list li:hover { background: var(--bb-parchment-light); }
.picker-list li.selected {
  background: rgba(168,40,29,0.08);
  color: var(--bb-ink);
  outline: 1px solid var(--bb-vermillion);
}
.picker-actions { margin-top: 12px; display: flex; gap: 8px; }
/* ---- Kind-chooser: 2-up card grid (#169) -------------------------------- */
/* Only two abstract choices (Special facility / Basic room), so give them
 * room rather than list rows — modeled on .theme-option-group's grid
 * mechanics, but square corners (no .theme-tile radius) per house style. */
.kind-option-group {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 10px;
  margin: 10px 0 4px;
}
@media (max-width: 420px) {
  .kind-option-group { grid-template-columns: 1fr; }
}
.kind-tile {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  padding: 12px;
  text-align: left;
  border: 1px solid var(--bb-rule);
  border-top-width: 3px;
  box-shadow: var(--shadow-card);
  background: var(--bb-parchment-light);
  cursor: pointer;
  font: inherit;
  color: var(--bb-ink);
  transition: border-color 0.12s;
}
.kind-tile.special { border-top-color: var(--bb-vermillion); }
.kind-tile.basic { border-top-color: var(--bb-sapphire); }
.kind-tile.special:hover { border-color: var(--bb-vermillion-deep); }
.kind-tile.basic:hover { border-color: var(--bb-sapphire-deep); }
.kind-tile:focus-visible { outline: 2px solid var(--bb-vermillion); outline-offset: -2px; }
.kind-tile-body { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
.kind-tile-body strong {
  font-family: var(--bb-font-display);
  font-size: 0.95rem;
  color: var(--bb-ink);
}
.kind-tile-body .desc {
  font-family: var(--bb-font-italic);
  font-style: italic;
  font-size: 0.8125rem;
  color: var(--bb-ink-soft);
}
/* Cap-blocked state: dim + the swapped-in reason text carries the meaning —
   never strike through the whole card, that reads as "broken/deleted". */
.kind-tile:disabled { opacity: 0.45; cursor: not-allowed; }
.kind-tile.special:disabled:hover,
.kind-tile.basic:disabled:hover { border-color: var(--bb-rule); }

/* Special-facility picker rows (#36) — keep the .pick-* hierarchy. */
.picker-list li:has(.pick-title) {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  padding: 9px 10px;
  border: 1px solid var(--bb-rule);
  margin-bottom: 6px;
}
.picker-list li:has(.pick-title):last-child { margin-bottom: 0; }
.picker-list .pick-body { flex: 1 1 auto; min-width: 0; }
.picker-list .pick-title {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 10px;
}
.picker-list .pick-name {
  font-family: var(--bb-font-display);
  font-size: 1.0625rem;          /* 17px */
  color: var(--bb-ink);
}
/* The level a chamber opens at, on the card. The open half is one alphabetical
   run, so this is the only place the level appears there — and it is what an
   owner who has just crossed a threshold scans for. */
.picker-list .pick-level {
  flex: 0 0 auto;
  font-family: var(--bb-font-display);
  font-size: 0.75rem;          /* 12px */
  letter-spacing: 0.04em;
  color: var(--bb-ink-soft);
  background: var(--bb-parchment-light);
  border: 1px solid var(--bb-rule-soft);
  border-radius: 0;
  padding: 1px 6px;
}
.picker-list .pick-meta,
.picker-list .pick-desc {
  margin-top: 3px;
  color: var(--bb-ink-soft);
  font-family: var(--bb-font-italic);
  font-style: italic;
  font-size: 0.75rem;          /* 12px */
}
.picker-list li:hover .pick-level {
  background: rgba(168,40,29,0.10);
  border-color: var(--bb-vermillion);
  color: var(--bb-vermillion-deep);
}
.picker-list li.selected .pick-level {
  color: var(--bb-vermillion-deep);
  background: rgba(168,40,29,0.10);
  border-color: var(--bb-vermillion);
}
.picker-list li:focus-visible { outline: 2px solid var(--bb-vermillion); outline-offset: -2px; }
.picker-list li.selected .pick-meta { color: var(--bb-ink-soft); }
.picker-list .pick-prereq {
  margin-top: 3px;
  color: var(--bb-gilt-deep);
  font-family: var(--bb-font-italic);
  font-style: italic;
  font-size: 0.75rem;          /* 12px */
}
/* THE SCHEDULE (#218) — the closed half of the commissioning book.
   Eligibility is carried by RUBRICATION: an open chamber is the bordered card
   above, a closed one is a name, a leader and its gate. That one distinction
   does two jobs — it says what may be built, and it is why a closed entry has
   no row affordances to withhold in the first place.
   The tier rule states the level gate once for every name beneath it, so a
   closed chamber spends ink only on a gate the reader can actually move. */
.pick-lead {
  display: flex;
  align-items: baseline;
  gap: 8px;
  margin: 0 0 10px;
  padding-bottom: 7px;
  border-bottom: 1px solid var(--bb-rule-soft);
  color: var(--bb-ink-soft);
  font-family: var(--bb-font-italic);
  font-style: italic;
  font-size: 0.8125rem;            /* 13px */
}
.pick-lead .pick-door { margin-left: auto; }
.pick-door {
  font: inherit;
  font-family: var(--bb-font-italic);
  font-style: italic;
  font-size: 0.78125rem;           /* 12.5px */
  background: none;
  border: 0;
  padding: 3px 0;
  border-bottom: 1px dotted var(--bb-gilt-deep);
  color: var(--bb-gilt-deep);
  white-space: nowrap;
  cursor: pointer;
}
.pick-door:hover {
  color: var(--bb-vermillion-deep);
  border-bottom-color: var(--bb-vermillion-deep);
}
.pick-door:focus-visible { outline: 2px solid var(--bb-vermillion); outline-offset: 2px; }

/* The band parting the two halves. Centred between hairlines rather than
   ruled-left like a tier, because it is not another group heading — it marks
   where "what you may build" ends and "what you may not, and why" begins. */
.picker-list li.pick-band {
  display: flex;
  align-items: center;
  gap: 10px;
  margin: 26px 0 4px;
  padding: 0;
  background: none;
  cursor: default;
}
.picker-list li.pick-band:hover { background: none; }
.picker-list li.pick-band:first-child { margin-top: 4px; }
.pick-band-t {
  flex: 0 0 auto;
  font-family: var(--bb-font-italic);
  font-style: italic;
  font-size: 0.8125rem;            /* 13px */
  color: var(--bb-ink-soft);
}
.picker-list li.pick-band .pick-rule-hair { transform: none; }

.picker-list li.pick-rule {
  display: flex;
  align-items: baseline;
  gap: 9px;
  margin: 16px 0 7px;
  padding: 0;
  background: none;
  cursor: default;
}
.picker-list li.pick-rule:first-child { margin-top: 2px; }
.picker-list li.pick-rule:hover { background: none; }
.pick-rule-t {
  font-family: var(--bb-font-display);
  font-size: 0.8125rem;            /* 13px */
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--bb-ink);
  white-space: nowrap;
}
.pick-rule-hair {
  flex: 1 1 auto;
  border-bottom: 1px solid var(--bb-rule);
  transform: translateY(-4px);
}
.pick-rule-state {
  font-family: var(--bb-font-italic);
  font-style: italic;
  font-size: 0.75rem;              /* 12px */
  color: var(--bb-ink-soft);
  white-space: nowrap;
}
.picker-list li.pick-closed {
  display: flex;
  align-items: baseline;
  gap: 9px;
  padding: 5px 2px;
  background: none;
  cursor: default;
}
.picker-list li.pick-closed:hover { background: none; }
.pick-closed-name {
  font-family: var(--bb-font-display);
  font-size: 0.9375rem;            /* 15px */
  color: var(--bb-ink-soft);
}
.pick-closed-lead {
  flex: 1 1 auto;
  min-width: 16px;
  border-bottom: 1px dotted var(--bb-rule);
  transform: translateY(-4px);
}
.pick-closed-gate {
  font-family: var(--bb-font-italic);
  font-style: italic;
  font-size: 0.78125rem;           /* 12.5px */
  color: var(--bb-gilt-deep);
  white-space: nowrap;
}

/* Owner selector row — multi-character bastions only; hidden for single-char. */
.picker-owner-row {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 4px 6px;
  margin: 0 0 8px;
}
.picker-owner-label {
  font-family: var(--bb-font-italic);
  font-style: italic;
  font-size: 0.8125rem;
  color: var(--bb-ink-soft);
}
.picker-owner-btn {
  font-size: 0.8125rem;
  font-family: var(--bb-font-ui);
  font-variant-numeric: lining-nums tabular-nums;
  border: 1px solid var(--bb-rule-soft);
  border-radius: 0;
  padding: 3px 10px;
  background: none;
  cursor: pointer;
  color: var(--bb-ink-soft);
  transition: color 0.1s, border-color 0.1s, background 0.1s;
}
.picker-owner-btn:hover { color: var(--bb-ink); border-color: var(--bb-ink-soft); }
.picker-owner-btn:focus-visible { outline: 2px solid var(--bb-gilt-deep); outline-offset: 1px; }
.picker-owner-btn.active {
  background: var(--bb-parchment-light);
  color: var(--bb-gilt-deep);
  border-color: var(--bb-gilt-deep);
  font-weight: 600;
}
.picker-owner-btn.at-cap { color: var(--bb-ink-faint); text-decoration: line-through; }
.picker-owner-btn.at-cap:hover { color: var(--bb-ink-faint); border-color: var(--bb-rule-soft); }

/* Special-facility picker heading — action masthead + faint filter readout. */
.picker-heading {
  margin: 0 0 10px;
  padding-bottom: 6px;
  border-bottom: 1px solid var(--bb-rule-soft);
  text-transform: none;
  letter-spacing: normal;
}
.picker-heading-action {
  display: block;
  font-family: var(--bb-font-display);
  font-size: 1.0625rem;          /* 17px */
  font-weight: 600;
  color: var(--bb-ink);
}
.picker-heading-filter {
  display: block;
  margin-top: 2px;
  font-family: var(--bb-font-italic);
  font-style: italic;
  font-size: 0.8125rem;            /* 13px */
  font-variant-numeric: lining-nums tabular-nums;
  color: var(--bb-ink-soft);
}
.picker-heading-filter:not(:empty)::before { content: "("; }
.picker-heading-filter:not(:empty)::after  { content: ")"; }
/* No action label above (redundant with the dialog title) — the filter is
 * the whole heading, so drop the parens and give it a touch more presence. */
.picker-heading-filter.standalone {
  margin-top: 0;
  font-size: 0.875rem;            /* 14px */
}
.picker-heading-filter.standalone::before,
.picker-heading-filter.standalone::after { content: none; }

/* ---- Picker · Responsive ------------------------------------------------ */
/* LAST in this section, and that is load-bearing rather than tidiness: a media
   query adds no specificity, so a tier rule and the unconditional rule it
   overrides are decided purely by SOURCE ORDER. Every rule here is
   single-surface (it touches nothing but this list), which is why it sits
   beside its surface instead of in the cross-surface phone band. */
@media (pointer: coarse) {
  /* The door is the only control the schedule has; the closed entries are
     text by construction and have nothing to tap. */
  .pick-door { display: inline-flex; align-items: center; min-height: 44px; }
}
@media (max-width: 700px), (max-height: 500px) and (pointer: coarse) {
  /* A tier rule cannot hold title + hairline + state on one 390px line: the
     nowrap title squeezes the state to nothing. Stack it — the title over its
     own hairline, the state beneath. */
  .picker-list li.pick-rule { display: block; margin: 18px 0 6px; }
  .picker-list li.pick-rule .pick-rule-t {
    display: block;
    padding-bottom: 4px;
    border-bottom: 1px solid var(--bb-rule);
  }
  .picker-list li.pick-rule .pick-rule-hair { display: none; }
  .pick-rule-state { display: inline; white-space: normal; }
  .pick-lead { display: block; }
  .pick-lead .pick-door { display: flex; width: fit-content; margin: 7px 0 0; }
  /* A gate is a sentence, and at this measure it wraps — so it must be allowed
     to, and the name/gate pair must wrap as a unit rather than overflow. */
  .picker-list li.pick-closed { flex-wrap: wrap; row-gap: 0; }
  .picker-list li.pick-closed .pick-closed-lead { min-width: 24px; }
  .pick-closed-gate { white-space: normal; }
}

/* ---- Misc small bits ---------------------------------------------------- */

.count {
  font-size: 0.875rem;
  color: var(--fg-dim);
  font-weight: 500;
  font-family: var(--font-ui);
  letter-spacing: 0.04em;
  font-variant-numeric: tabular-nums;
}
/* The status chip is styled per-state: idle/active leave it as plain text
   (no pill — see §6.1), and only tool-mode renders it as a pill via
   `.placement-banner.mode #placement-status` (#131 removed the legacy
   unscoped `#placement-status:not(:empty)` rule that forced a red pill on
   every state, including idle). */

/* ---- Summary bar -------------------------------------------------------- */

.summary {
  display: flex;
  flex-wrap: wrap;
  gap: 28px;
  align-items: center;
}
.summary strong {
  color: var(--accent);
  font-family: var(--font-display);
  font-size: 1.4em;
  font-weight: 700;
  letter-spacing: -0.01em;
}
.status {
  margin-left: auto;
  color: var(--fg-dim);
  font-size: 0.84rem;
  font-style: italic;
  font-family: var(--font-display);
}
.status.success { color: var(--success); }
.status.error { color: var(--danger); }

.empty {
  color: var(--fg-dim);
  font-style: italic;
  font-family: var(--font-display);
  padding: 8px 4px;
}

/* =========================================================================
 * FLOORPLAN
 * ========================================================================= */

.hint {
  font-size: 0.84rem;
  color: var(--fg-dim);
  max-width: 380px;
  text-align: right;
  font-style: italic;
  font-family: var(--font-display);
  line-height: 1.4;
}

/* #floor-stack holds the single active floor's .plate-frame (#160). */
.floor-stack {
  display: flex;
  flex-direction: column;
  min-width: 0;
}

/* The "drafting surface" (Codex §6.5) — warm vellum + faint sepia-ink
 * prick noise + matted frame. The only place in Codex chrome that uses
 * a textured fill rather than flat parchment. */
.floor-canvas {
  position: relative;
  background:
    /* sepia ink-prick noise */
    radial-gradient(var(--bb-canvas-fleck) 1px, transparent 1px) 0 0 / 3px 3px,
    /* warm vellum tint */
    linear-gradient(180deg,
        var(--bb-parchment-light) 0%,
        var(--bb-parchment-mid)   100%);
  border: 2px solid var(--bb-wall);
  border-radius: 0;
  /* Matted frame: a 4px parchment outline + an outer rule-soft hairline. */
  outline: 4px solid var(--bb-parchment);
  outline-offset: 0;
  padding: 10px;
  /* Matted frame always bounds the grid; placement-mode peek SVG still
   * bleeds via overflow:visible on the wrapper. Wide bastions scroll
   * horizontally via .stage. */
  overflow: visible;
  flex: 0 0 auto;
  box-shadow:
    0 0 0 5px var(--bb-rule-soft),
    inset 0 1px 0 var(--bb-inset-light),
    inset 0 0 24px var(--bb-shadow-inset-1);
}
.floor-empty-hint {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  pointer-events: none;
  font-family: var(--bb-font-italic);
  font-style: italic;
  font-size: 0.875rem;
  color: var(--bb-ink-soft);
  text-align: center;
  padding: 12px;
}
/* Inline "Commission a facility" link in the empty-floor hint (R2.2). The hint
 * is pointer-events:none so grid clicks pass through — re-enable just the link.
 * Mirrors the .char-focus-btn vocabulary (the rule is scoped to
 * .charter-identity, so restate it here): italic, dotted underline, vermillion. */
.floor-empty-hint .char-focus-btn {
  pointer-events: auto;
  background: none;
  border: none;
  padding: 0;
  cursor: pointer;
  font-family: var(--bb-font-italic);
  font-style: italic;
  font-size: inherit;
  color: var(--bb-vermillion);
  text-decoration: underline dotted;
  text-underline-offset: 2px;
}
.floor-empty-hint .char-focus-btn:hover { color: var(--bb-vermillion-deep); }
.floor-canvas svg {
  display: block;
  user-select: none;
}

/* Four-direction grow (#43): the SVG is wrapped in a div sized to the
 * committed grid so the on-screen origin stays pinned when the left/top
 * peek bands open during placement. The SVG itself bleeds outward via
 * negative absolute positioning, painting over neighboring UI (the
 * floor-side panel, the previous floor row, etc.) for the duration of
 * the placement gesture. With all peeks at 0 the SVG sits exactly on
 * the wrapper — visually identical to the pre-#43 layout. */
.floor-svg-wrap {
  position: relative;
  overflow: visible;
  flex: 0 0 auto;
}
.floor-svg-wrap > .floor-svg {
  position: absolute;
  /* Let the grid edge-drag handles (#129) bleed ~4px past the grid rim
   * without being clipped by the SVG's default overflow:hidden. */
  overflow: visible;
}

/* ---- SVG overlays inside the floor canvas (Codex §6.5) ------------------ */

/* Below-floor shadow (§6.5.12) — faint outlines + dashed walls. */
.below-outline {
  fill: var(--bb-below-wash);
  pointer-events: none;
}
.below-outline-group { pointer-events: none; }
.below-wall {
  stroke: var(--bb-hairline);
  stroke-width: 2;
  stroke-linecap: square;
  stroke-dasharray: 4 3;
  pointer-events: none;
}
.below-defensive-wall {
  fill: var(--bb-stone-fill-ghost);
  stroke: var(--bb-hairline);
  stroke-width: 1;
  stroke-dasharray: 3 2;
  pointer-events: none;
}
/* Opening projected a floor down (#242): the ghost tint without the wall's
 * dashed border, so it reads as the passable stretch of the underlaid ring. */
.below-defensive-opening {
  fill: var(--bb-stone-fill-ghost);
  pointer-events: none;
}
/* Below-floor stair shadow (#160) — structural footprint outline, both paths.
 * Export inks it in hairline to sit with the other export below-* linework; the
 * editor underlay overrides to --bb-wall below. */
.below-stair {
  fill: none;
  stroke: var(--bb-hairline);
  stroke-width: 1.5;
  stroke-dasharray: 4 3;
  pointer-events: none;
}

/* Ghost underlay (#160, §5.9): the editor-only tracing of lower floors. The
 * export shadow pass is promoted to Revit-style halftone linework — structural
 * only (no fills / icons / names), composited ONCE via this group's opacity so
 * dashed joins never double-darken. Inner linework runs near full strength
 * (depth-1 = 1.0); this ~0.24 group carries the ghost presence. Export renders
 * the same classes WITHOUT this ancestor, so it keeps today's exact treatment. */
.floor-underlay { opacity: 0.24; }
/* Structural ink: walls, facility footprints (as perimeters, not washes), and
 * stairs all trace in --bb-wall. */
.floor-underlay .below-wall,
.floor-underlay .below-stair { stroke: var(--bb-wall); }
.floor-underlay .below-outline {
  fill: none;
  stroke: var(--bb-wall);
  stroke-width: 1.5;
}
/* Corridor wash — faint circulation fill, editor underlay only. */
.below-corridor {
  fill: var(--bb-below-wash);
  pointer-events: none;
}

/* Placement-mode peek bands (§6.5.4). */
.bg-canvas {
  fill: var(--bb-parchment-light);
}
.grid-line {
  stroke: var(--bb-grid-line);
  stroke-width: 1;
}
.grid-peek {
  fill: var(--bb-peek-wash);
  pointer-events: none;
}
.grid-edge {
  stroke: var(--bb-vermillion);
  stroke-width: 1.5;
  stroke-dasharray: 6 4;
  pointer-events: none;
  opacity: 0.85;
}

/* ---- Placed facility rects (§6.5.1) ------------------------------------- */
/* Hue-coded: special = vermillion, basic = sapphire (decision #1). */

.facility-rect {
  cursor: pointer;
  transition: filter 0.12s, fill 0.12s, stroke 0.12s, stroke-width 0.12s;
  filter: drop-shadow(0 1px 1px var(--bb-hairline));
}
@media (prefers-reduced-motion: reduce) {
  .facility-rect { transition: none; }
}
.facility-rect:hover {
  filter: brightness(1.05) drop-shadow(0 2px 3px var(--bb-shadow-deep));
}
.facility-rect.special {
  fill: var(--bb-special-fill);
  stroke: var(--bb-vermillion-deep);
  stroke-width: 2;
}
.facility-rect.basic {
  fill: var(--bb-basic-fill);
  stroke: var(--bb-sapphire-deep);
  stroke-width: 2;
}

/* ---- Walls / openings / doors (§6.5.5) ---------------------------------- */

.wall {
  stroke: var(--bb-wall);
  stroke-width: 4;
  stroke-linecap: square;
  pointer-events: none;
}
/* Interior partition tier (#178): a facility's inner fixtures — closet /
 * washroom walls and their doors, windows, arches — read as one dimmer layer
 * (internal subdivisions, not the building's shell). renderFloorSvg collects
 * the whole interior network into ONE `.interior-partitions` group, so a
 * single group opacity flattens it: overlapping strokes composite once and a
 * closet reads as an even tier. Per-element opacity was wrong — two 0.6 square
 * linecaps meeting at a join stack to ~0.84, a darker dot at every corner.
 * Walls keep a thinner line-weight as a second cue. Mirror to export-svg.js. */
.interior-partitions {
  opacity: 0.6;
}
/* Second interior cue: a thinner line than the exterior shell (the drafting
 * convention — inner partitions read lighter than load-bearing walls). Applies
 * to the interior wall AND the structural wall-jambs of interior fixtures
 * (window / arch / secret-door) so a jamb sitting inline with an interior wall
 * matches its weight. Door leaf / swing / panel glyphs keep their own intrinsic
 * weight — they're glyphs, not wall lines. Mirror any change to export-svg.js. */
.wall.interior,
.window.interior .window-wall,
.arch.interior .arch-jamb,
.door.interior .door-secret-wall {
  stroke-width: 3;
}
.opening {
  stroke: var(--bb-wall);
  stroke-width: 2;
  stroke-dasharray: 3 3;
  opacity: 0.35;
  pointer-events: none;
}
/* Arch (#175): wall jambs frame a door-length gap (supports), so two adjacent
 * arches leave a column between them — distinct from a wall-less opening. */
.arch { pointer-events: none; }
.arch-jamb {
  stroke: var(--bb-wall);
  stroke-width: 4;
  stroke-linecap: square;
  pointer-events: none;
}
/* Door glyph (#128): hinge nub + leaf + quarter swing arc. Stroke weight
 * hierarchy: wall 4 > leaf 2 > arc 1 (arc is the motion trace, leaf is the
 * door itself). Arc is solid — dash is .opening's language. */
.door {
  fill: none;
  pointer-events: none;
}
.door-hinge {
  fill: var(--bb-vermillion-deep);
  stroke: none;
}
.door-leaf {
  stroke: var(--bb-vermillion-deep);
  stroke-width: 2;
  stroke-linecap: round;
}
.door-swing {
  fill: none;
  stroke: var(--bb-vermillion);
  stroke-width: 1;
}
/* Pre-#128 panel style — the per-bastion alternative to swing arcs
 * (Settings → Floorplan → Show door swings). */
.door-panel {
  fill: var(--bb-vermillion-light);
  stroke: var(--bb-vermillion-deep);
  stroke-width: 1;
  pointer-events: none;
}
/* Hover preview of the next swing position (doors mode, never exported). */
.door-ghost {
  opacity: 0.5;
}
.door-ghost .door-leaf,
.door-ghost .door-swing {
  stroke-dasharray: 4 3;
}
/* Ghost door panel (doors-mode hover on an empty edge, arcs off): a dashed
 * vermillion outline so it reads as a door preview, distinct from the Walls
 * tool's solid hover line. */
.door-panel.door-ghost {
  fill: none;
  stroke-dasharray: 3 2;
}
/* Muted when the click will remove the door, vermillion when it will add one. */
.door-panel.door-ghost.remove {
  stroke: var(--bb-accent-muted);
}

/* Window (#175): the wall barrier stays, with two thin glaze bars parallel to
 * it (the drafting double-tick). Drawn in the wall ink — never a family hue. */
.window-wall {
  stroke: var(--bb-wall);
  stroke-width: 4;
  stroke-linecap: square;
  pointer-events: none;
}
/* Opaque pane matching the empty-grid ground, so a window reads the same over
 * any facility fill (basic/special) as it does on an external wall. */
.window-pane {
  fill: var(--bb-parchment);
  stroke: none;
  pointer-events: none;
}
/* Thin double glazing line bridging the gap — lighter than a door. */
.window-bar {
  stroke: var(--bb-wall);
  stroke-width: 1;
  pointer-events: none;
}
/* Door kinds (#175): added treatment on the vermillion door group — never a hue
 * swap, so a door can still flip to a state hue with its kind marks intact. */
.door-bolt { fill: var(--bb-vermillion-deep); stroke: none; pointer-events: none; }
.door-secret-wall {
  stroke: var(--bb-wall);
  stroke-width: 4;
  stroke-linecap: square;
  pointer-events: none;
}
/* Secret: the wall reads unbroken, marked by the classic "S" map convention —
 * an actual capital-S glyph so it reads crisply at any size. */
.door-secret-s {
  fill: var(--bb-vermillion-deep);
  /* Theme-invariant sans (see --bb-font-map) — crisp as a symbol and identical
   * in the SVG export / handout, which can't embed the codex web fonts. */
  font-family: var(--bb-font-map, Arial, Helvetica, sans-serif);
  font-weight: 500;
  font-size: 17px;
  stroke: none;
  pointer-events: none;
}
/* Portcullis: a symmetric vermillion grille dropped into a breached wall (the
 * faint-dash opening ground) — so it reads as a gate, not a decorated wall. */
.door-portcullis-bar {
  stroke: var(--bb-vermillion-deep);
  stroke-width: 1.5;
  stroke-linecap: round;
  pointer-events: none;
}
/* Ghost preview for window/arch placement (openings mode; never exported). */
.opening-ghost { opacity: 0.55; }

/* ---- Corridor cells (§6.5.6) -------------------------------------------- */

.corridor {
  fill: var(--bb-corridor-fill);
  pointer-events: none;
}
/* Corridor-family kinds (#179) — every kind shares the .corridor fill; the
 * kind reads from line-work alone (§5.9 class-by-treatment). Shared rules,
 * not per-theme: the strokes reference existing per-theme tokens. */
.corridor-walkway-hatch {
  pointer-events: none;
  stroke: var(--bb-stone-hatch);
  stroke-width: 0.75;
}
.corridor-balcony-edge {
  pointer-events: none;
  stroke: var(--bb-wall);
  stroke-width: 1;
  stroke-dasharray: 1.5 2;
  fill: none;
}
.corridor-hover {
  pointer-events: none;
  fill: rgba(80,40,10,0.12);
  stroke: var(--bb-vermillion);
  stroke-width: 1.5;
  stroke-dasharray: 4 3;
}
.corridor-hover.remove { stroke: var(--bb-accent-muted); }
/* .retype (convert-in-place) keeps the add accent — the armed kind's faint
 * signature line-work inside the outline is what marks it as a conversion. */
.corridor-hover-preview .corridor-walkway-hatch,
.corridor-hover-preview .corridor-balcony-edge { opacity: 0.55; }

/* ---- Terrain paint (#143, blob contours + six kinds #236) --------------- */
/* Ground materials beneath everything built. Hue-for-material is the
 * sanctioned §5.9 carve-out — kind also reads from line-work plus texture,
 * never color alone. Contour matrix invariant: no two kinds share both
 * stroke-width and dash (water 1.25 solid · road 1.0 solid butt · grass
 * 0.75/1.5 3 · sand 1.0/2 2.5 · rock 1.25/5 1.5 butt · marsh 1.0/4 2). */

.terrain { pointer-events: none; }
.terrain-water { fill: var(--bb-terrain-water); }
.terrain-road  { fill: var(--bb-terrain-road); }
.terrain-grass { fill: var(--bb-terrain-grass); }
.terrain-sand  { fill: var(--bb-terrain-sand); }
.terrain-rock  { fill: var(--bb-terrain-rock); }
.terrain-marsh { fill: var(--bb-terrain-marsh); }
.terrain-edge { pointer-events: none; fill: none; }
.terrain-water-edge {
  stroke: var(--bb-terrain-water-line);
  stroke-width: 1.25;
  stroke-linecap: round;
}
.terrain-road-edge {
  stroke: var(--bb-terrain-road-line);
  stroke-width: 1;
  stroke-linecap: butt;
}
.terrain-grass-edge {
  stroke: var(--bb-terrain-grass-line);
  stroke-width: 0.75;
  stroke-dasharray: 1.5 3;
  stroke-linecap: round;
}
.terrain-sand-edge {
  stroke: var(--bb-terrain-sand-line);
  stroke-width: 1;
  stroke-dasharray: 2 2.5;
  stroke-linecap: round;
}
.terrain-rock-edge {
  stroke: var(--bb-terrain-rock-line);
  stroke-width: 1.25;
  stroke-dasharray: 5 1.5;
  stroke-linecap: butt;
}
.terrain-marsh-edge {
  stroke: var(--bb-terrain-marsh-line);
  stroke-width: 1;
  stroke-dasharray: 4 2;
  stroke-linecap: round;
}
.terrain-water-ripple {
  pointer-events: none;
  fill: none; /* tilde <path> — an unset fill would render as a black blob */
  stroke: var(--bb-terrain-water-line);
  stroke-width: 0.75;
  stroke-linecap: round;
  opacity: 0.55;
}
.terrain-road-rut {
  pointer-events: none;
  fill: none; /* bend ruts are open <path> arcs — an unset fill closes + fills them into a dark crescent at every corner (#218) */
  stroke: var(--bb-terrain-road-line);
  stroke-width: 0.6;
  stroke-dasharray: 5 3;
  stroke-linecap: round;
  opacity: 0.5;
}
.terrain-grass-tuft {
  pointer-events: none;
  stroke: var(--bb-terrain-grass-line);
  stroke-width: 0.6;
  stroke-linecap: round;
}
.terrain-sand-stipple {
  pointer-events: none;
  stroke: var(--bb-terrain-sand-line);
  stroke-width: 1.1;
  stroke-linecap: round;
  opacity: 0.45;
}
.terrain-rock-hatch {
  pointer-events: none;
  stroke: var(--bb-terrain-rock-line);
  stroke-width: 0.8;
  stroke-linecap: butt;
}
.terrain-marsh-reed {
  pointer-events: none;
  fill: none; /* wavelet <path> shares the class with the reed lines */
  stroke: var(--bb-terrain-marsh-line);
  stroke-width: 0.6;
  stroke-linecap: round;
}
.terrain-hover {
  pointer-events: none;
  fill: rgba(80,40,10,0.12);
  stroke: var(--bb-vermillion);
  stroke-width: 1.5;
  stroke-dasharray: 4 3;
}
.terrain-hover.remove { stroke: var(--bb-accent-muted); }
/* Fill preview (#293): what a single click would change. Same vermillion
 * dashed language as the one-cell hover above, so it reads as the same tool
 * speaking about more ground — but the wash is much fainter than the hover
 * rect's, because it can cover hundreds of squares and the hover's weight
 * across a whole quarter of the map would out-shout the map. The wash is
 * clipped to open ground by the node builder, not by CSS: buildings inside the
 * region must not tint, or the preview reads as a selection of them. */
.terrain-fill-preview { pointer-events: none; }
.terrain-fill-wash { fill: rgba(80,40,10,0.11); }
.terrain-fill-edge {
  fill: none;
  stroke: var(--bb-vermillion);
  stroke-width: 1.5;
  stroke-dasharray: 4 3;
}
.terrain-fill-wash.remove { fill: rgba(80,40,10,0.08); }
.terrain-fill-edge.remove { stroke: var(--bb-accent-muted); }
/* Upper-floor echo: group opacity set inline by drawBelowTerrain. */
.below-terrain { pointer-events: none; }

/* ---- Vehicle environment wash (#238) ------------------------------------ */
/* A fixed decorative backdrop derived from bastion_type alone — no tool, no
 * per-cell state. Drawn into .vehicle-hull-layer, so the grid ruling paints
 * over it and the opaque deck plate restores plain parchment inside the hull.
 * The wavelet takes slightly more weight than its terrain cousin (0.75): these
 * marks are ~9× sparser and have no competing terrain to out-shout. */
.vehicle-wash { pointer-events: none; }
.vehicle-wash-water   { fill: var(--bb-vehicle-water); }
.vehicle-wash-deep    { fill: var(--bb-vehicle-water-deep); }
.vehicle-wash-sky     { fill: var(--bb-vehicle-sky); }
.vehicle-wash-ballast { fill: var(--bb-vehicle-ballast); }
.vehicle-wash-ripple {
  pointer-events: none;
  fill: none; /* tilde <path> — an unset fill renders as a black blob */
  stroke: var(--bb-vehicle-water-line);
  stroke-width: 0.85;
  stroke-linecap: round;
  opacity: 0.6;
}
.vehicle-wash-swell {
  pointer-events: none;
  fill: none;
  stroke: var(--bb-vehicle-water-line);
  stroke-width: 0.6;
  stroke-linecap: round;
  opacity: 0.32;
}
.vehicle-wash-stratus {
  pointer-events: none;
  stroke: var(--bb-vehicle-sky-line);
  stroke-width: 0.9;
  stroke-linecap: round;
  opacity: 0.5;
}
.vehicle-wash-grit {
  pointer-events: none;
  stroke: var(--bb-vehicle-ballast-line);
  stroke-width: 1.1;
  stroke-linecap: round;
  opacity: 0.4;
}
/* Conductor stones — the enchanted line a lightning rail floats above, not a
 * pair of iron rails. The only saturated accent in any wash, and the heaviest
 * mark on the canvas: it is a made, working thing among natural texture. The
 * glow is a second, larger diamond behind each stone — the export pipeline
 * carries no filters, so the halo has to be geometry. */
.vehicle-wash-conductor-thread {
  pointer-events: none;
  stroke: var(--bb-vehicle-conductor);
  stroke-width: 0.9;
  stroke-dasharray: 1.5 3;
  opacity: 0.5;
}
.vehicle-wash-conductor-glow {
  pointer-events: none;
  fill: var(--bb-vehicle-conductor-glow);
  stroke: none;
}
.vehicle-wash-conductor {
  pointer-events: none;
  fill: var(--bb-vehicle-conductor);
  stroke: none;
}
/* Waterline collar — a fat, faint restatement of the hull path drawn UNDER the
 * opaque deck plate, so only a ~3px ring survives outside the hull. Geometry,
 * not effects: the export pipeline carries no filters or <defs>. */
.vehicle-waterline {
  pointer-events: none;
  fill: none;
  stroke: var(--bb-vehicle-water-line);
  stroke-width: 11;
  opacity: 0.35;
  stroke-linejoin: round;
  stroke-linecap: round;
}
/* Deck plate — hull interior restored to plain canvas, so a deck reads exactly
 * as a stationary floorplan does. Same fill as .bg-canvas. */
.vehicle-deck-plate {
  pointer-events: none;
  fill: var(--bb-parchment-light);
  stroke: none;
}

/* ---- Defensive walls (§6.5.7) ------------------------------------------- */
/* Warm-stone fill with a diagonal hatch — visibly heavier than the corridor
 * sepia wash at the same z-level. */

.defensive-wall {
  pointer-events: none;
  fill: var(--bb-stone-fill);
  stroke: var(--bb-wall);
  stroke-width: 1;
}
.defensive-wall-hatch {
  pointer-events: none;
  stroke: var(--bb-stone-hatch);
  stroke-width: 1;
}
.defensive-wall-hover {
  pointer-events: none;
  fill: rgba(120,90,50,0.20);
  stroke: var(--bb-vermillion);
  stroke-width: 1.5;
  stroke-dasharray: 4 3;
}
.defensive-wall-hover.remove { stroke: var(--bb-accent-muted); }

/* Opening in the defensive ring (§6.5.8, #242): gate + drawbridge share this
 * ghosted-stone tint (the upper-course colour) so the cell still reads as wall
 * — passable, but a visible continuation of the masonry, and still enclosing. */
.defensive-opening {
  pointer-events: none;
  fill: var(--bb-stone-fill-ghost);
}
/* Gate leaf: a gilt-deep bar across the passage, centred in the wall thickness
 * — the shut gate filling the opening. One mark over the ghost tint. */
.defensive-gate-bar {
  pointer-events: none;
  stroke: var(--bb-gilt-deep);
  stroke-width: 3;
  /* Butt caps, not round: the bar must end exactly on the cell edge — round
   * caps spill 1.5px into the neighbours, and butt caps let multi-cell gates
   * meet seamlessly at the shared edge. */
  stroke-linecap: butt;
  fill: none;
}
.gate-hover {
  pointer-events: none;
  fill: rgba(120,90,50,0.10);
  stroke-width: 1.5;
  stroke-dasharray: 4 3;
}
.gate-hover.to-gate { stroke: var(--bb-gilt-deep); }
.gate-hover.to-remove { stroke: var(--bb-accent-muted); }

/* Drawbridge (#242): a lowered planked deck projecting out over the moat from a
 * gate cell. Two figure/ground families — the gilt-deep frame (side rails + far
 * lip) reads chromatically as the gate it extends, the ink-soft surface (cross-
 * wise planks + splayed chains) as deck line-work. Stroke-only, per the register
 * spec; kind is treatment on the group, never a new hue (#175). */
.drawbridge-deck-fill {
  pointer-events: none;
  fill: var(--bb-drawbridge-fill);
  stroke: none;
}
/* Deck side rails: ink-soft line-work — the deck's own edge, not gate structure.
 * Only the far lip is gilt (the landing that ties back to the gate chroma). */
.drawbridge-rail {
  pointer-events: none;
  stroke: var(--bb-ink-soft);
  stroke-width: 1;
  fill: none;
}
.drawbridge-lip {
  pointer-events: none;
  stroke: var(--bb-gilt-deep);
  stroke-width: 2;
  stroke-linecap: round;
  fill: none;
}
.drawbridge-plank,
.drawbridge-chain {
  pointer-events: none;
  stroke: var(--bb-ink-soft);
  stroke-width: 1;
  fill: none;
}
.drawbridge-chain {
  stroke-linecap: round;
  stroke-dasharray: 3 2;
}
.drawbridge-pivot {
  pointer-events: none;
  fill: var(--bb-ink-soft);
  stroke: none;
}

/* ---- Stairs (§6.5.9) ---------------------------------------------------- */
/* Sapphire — the "info ink" — reads as stairs at every scale. */

.stair { pointer-events: none; }
.stair-bg     { fill: var(--bb-basic-fill-stair); }
.stair-step {
  stroke: var(--bb-sapphire-deep);
  stroke-width: 1.2;
  stroke-linecap: round;
}
.stair-arrow-shaft {
  stroke: var(--bb-sapphire-deep);
  stroke-width: 1.5;
  stroke-linecap: round;
  fill: none;
}
.stair-arrow-head {
  fill: none;
  stroke: var(--bb-sapphire-deep);
  stroke-width: 1.5;
  stroke-linecap: round;
  stroke-linejoin: round;
}
/* Fill-state direction encoding (#182): hollow head = up, solid head = down.
 * Rotation-proof — the fill never flips meaning, unlike an arrow that spins
 * with the glyph. */
.stair-arrow-head.up   { fill: none; }
.stair-arrow-head.down { fill: var(--bb-sapphire-deep); }
/* Break line where the run passes through this floor's slice (#182) — the
 * drafting cut: solid below it, ghosted or ink-heavy beyond it. */
.stair-cut {
  stroke: var(--bb-sapphire-deep);
  stroke-width: 0.8;
  fill: none;
  opacity: 0.7;
  stroke-linecap: round;
}
/* UP/DN tags on the big flights (grand 2×2, ramp 1×2) — 1×1 plates have no
 * room and rely on the fill-state head alone. Typography is inline per node
 * (survives standalone export); the halo matches the annotation idiom. */
.stair-label {
  fill: var(--bb-sapphire-deep);
  text-anchor: middle;
  paint-order: stroke;
  stroke: var(--bb-halo);
  stroke-width: 2px;
  stroke-linejoin: round;
}
/* Open gable chevron marking a roof hatch (#144) — pitched-roof silhouette. */
.stair-roof {
  fill: none;
  stroke: var(--bb-sapphire-deep);
  stroke-width: 1.5;
  stroke-linecap: round;
  stroke-linejoin: round;
}
/* Grand's flared ceremonial base — the bullnose that reads it as one grand
 *  flight rather than four tiled straights (#176). */
.stair-bullnose {
  fill: none;
  stroke: var(--bb-sapphire-deep);
  stroke-width: 1.5;
  stroke-linecap: round;
  stroke-linejoin: round;
}
/* Ramp rails — run ALONG the slope (inverted from stair treads), so a ramp
 *  never reads as stairs (#176). */
.stair-rail {
  stroke: var(--bb-sapphire-deep);
  stroke-width: 1.5;
  stroke-linecap: round;
}
/* Spiral stair-well boundary — the round shaft the treads wind inside (#176). */
.stair-well {
  fill: none;
  stroke: var(--bb-sapphire-deep);
  stroke-width: 1.2;
}
.stair-hover {
  pointer-events: none;
  fill: var(--bb-basic-fill);
  stroke-width: 1.5;
  stroke-dasharray: 4 3;
}
.stair-hover.add    { stroke: var(--bb-vermillion); }
.stair-hover.remove { stroke: var(--bb-accent-muted); }

/* ---- Annotations (§6.5.10) ---------------------------------------------- */
/* Italic IM Fell — feels handwritten in the margins. */

/* Typography on .annotation-text is driven by inline style from annotationRecipe()
   in annotation-styles.js — one source, no cascade-override surprises. */
.annotation { pointer-events: none; }
.annotation-text {
  fill: var(--bb-ink);          /* fallback if recipe inline style is absent */
  paint-order: stroke;
  stroke: var(--bb-halo);
  stroke-width: 3px;
  stroke-linejoin: round;
}
body[data-edit-mode="annotations"] .annotation {
  pointer-events: all;
  cursor: grab;
}
/* Keyed markers are always interactive so the tooltip works in every mode */
.annotation-keyed {
  pointer-events: all;
}
.annotation-keyed:focus {
  outline: 2px solid var(--bb-vermillion);
  outline-offset: 2px;
}
.annotation-hover {
  pointer-events: none;
  fill: var(--bb-special-fill-wash);
  stroke: var(--bb-vermillion);
  stroke-width: 1.5;
  stroke-dasharray: 4 3;
}

/* Hover edge highlight in walls/doors mode (§6.5.5). */
.wall-hover {
  stroke-width: 4;
  stroke-linecap: square;
  stroke-dasharray: 5 3;
  pointer-events: none;
}
.wall-hover.add    { stroke: var(--bb-vermillion);      opacity: 0.85; }
.wall-hover.remove { stroke: var(--bb-accent-muted); opacity: 0.85; }

/* ---- Ghost rect + drag handles (§6.5.3) --------------------------------- */

.ghost-group { pointer-events: none; }
.ghost-rect {
  pointer-events: none;
  stroke-dasharray: 5 3;
  stroke-width: 2;
}
.ghost-rect.ok {
  fill: rgba(74,111,58,0.32);
  stroke: var(--bb-verdigris);
}
/* Upgrade tier (#37): gilt sits between verdigris (ok) and vermillion (bad)
 * so the user reads it as "different from both — committing also bumps
 * the size and charges its cost/days". */
.ghost-rect.upgrade {
  fill: var(--bb-gilt-fill-strong);
  stroke: var(--bb-gilt);
}
.ghost-rect.bad {
  fill: var(--bb-danger-fill);
  stroke: var(--bb-danger);
}
/* Adjust (parked) phase: solid stroke distinguishes from the follow-phase
 * dashed ghost; pointer-events re-enabled so body drag and tap-to-commit work. */
.ghost-rect.parked {
  stroke-dasharray: none;
  pointer-events: all;
  cursor: grab;
}
.ghost-group.dragging .ghost-rect.parked {
  cursor: grabbing;
}

/* w×h label anchored at the ghost's top-left — mono nums with a parchment-
 * light halo so it reads against any of the three tier fills. */
.ghost-label {
  pointer-events: none;
  font-family: var(--bb-font-mono);
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.02em;
  paint-order: stroke fill;
  stroke: var(--bb-halo);
  stroke-opacity: 0.85;
  stroke-width: 3;
  stroke-linejoin: round;
}
.ghost-label.ok      { fill: var(--bb-verdigris-deep); }
.ghost-label.upgrade { fill: var(--bb-gilt-deep); }
.ghost-label.bad     { fill: var(--bb-accent-muted); }

/* 8 drag handles drawn inside the ghost group (§6.5.3). The group is
 * pointer-events: none; the handles re-enable hit-testing so drag begins
 * from a handle, never from the rect body. Per-handle cursors are set
 * inline by the renderer so nw/n/ne/e/se/s/sw/w-resize lands immediately. */
.ghost-handle {
  pointer-events: all;
  fill: var(--bb-parchment-light);
  stroke: var(--bb-vermillion-deep);
  stroke-width: 1.5;
  transition: transform 0.08s ease-out;
  transform-box: fill-box;
  transform-origin: center;
}
.ghost-handle:hover {
  fill: var(--bb-vermillion);
  transform: scale(1.25);
}

/* Facility icon (§6.5.2). currentColor on the SVG paths picks up the
 * per-kind color set below. The parchment-light halo (drop-shadow) keeps
 * the icon legible against the tinted facility fill. */
.facility-icon {
  pointer-events: none;
  transition: filter 0.12s;
  filter: drop-shadow(0 0 1px var(--bb-halo));
}
.facility-icon.special { color: var(--bb-vermillion-deep); }
.facility-icon.basic   { color: var(--bb-sapphire-deep); }
.facility-icon.haunted { color: var(--bb-haunted); }
.facility-icon.construction { color: var(--bb-gilt-deep); }
/* Auto-furnish props (#184) — floor markings, not chrome: secondary ink, no
 * halo/drop-shadow. Quieter than the facility icon/label, louder than grid. */
.facility-icon.furnishing { color: var(--bb-ink-soft); filter: none; }
.facility-deco.hovered .facility-icon {
  filter: drop-shadow(0 0 2px var(--bb-halo)) brightness(1.05);
}

/* Decoration layer sits above walls so icons/names/× stay legible when a
 * wall crosses through. Pointer events pass through to the rect group
 * underneath; the × button re-enables them when visible. */
.facility-deco { pointer-events: none; }

/* In-tile facility name. Bottom-aligned, only rendered when it fits — the
 * parchment-light stroke gives a halo so it stays legible against either
 * the vermillion or sapphire fill tint. */
.facility-name {
  font-family: var(--bb-font-display);
  font-weight: 500;
  /* font-size is set inline per node, scaled to CELL_PX (#160 label-scale) —
   * see placedDecorationsNode in render-svg-nodes.js. */
  letter-spacing: 0.01em;
  fill: var(--bb-ink);
  pointer-events: none;
  paint-order: stroke;
  stroke: var(--bb-halo);
  stroke-width: 3px;
  stroke-linejoin: round;
}

.x-btn {
  cursor: pointer;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.1s ease;
}
/* Idle-only (#186): the × sits over the tile's top-right corner, so keeping it
 * live in a drawing tool would swallow clicks meant for stairs/notes/openings
 * there. .map-idle is set on the SVG in Select & Move mode only. */
.map-idle .facility-deco.hovered .x-btn {
  opacity: 1;
  pointer-events: auto;
}
.x-btn rect {
  fill: var(--bb-parchment-light);
  stroke: var(--bb-danger);
}
.x-btn text {
  fill: var(--bb-danger);
  font-size: 13px;
  font-family: var(--bb-font-display);
  font-weight: 600;
}
.x-btn:hover rect { fill: var(--bb-danger); }
.x-btn:hover text { fill: var(--bb-parchment-light); }
/* =========================================================================
 * The app shell (#121)
 * =========================================================================
 * The five-chapter shell and everything hung off it: the controls-only top
 * bar, the chapter tabs, the page body, and the per-chapter surfaces.
 *
 * The title here read "Two-pane layout overrides for the Bastion Builder
 * mockup … layered on top of styles.v2.css" until #298 — naming a prototype
 * and a file that has not existed for years, above the largest run of live
 * rules in the sheet. It is the specimen the table of contents was built to
 * catch: nothing was wrong with the RULES, only with the sign over them.
 * ========================================================================= */

body {
  padding: 0;
  min-height: 100vh;
}

/* ---- App shell ---------------------------------------------------------- */

.app-shell {
  display: grid;
  grid-template-rows: auto auto 1fr;   /* top-bar · chapter-nav · chapter-region */
  /* minmax(0, 1fr), not the implicit `auto` (#218). An `auto` column grows to
   * its widest item's min-content, so any content wider than the viewport —
   * the floorplan SVG, or a register row set in a wide display face — dragged
   * the column past the window and took the top bar and every panel with it,
   * right-anchoring the ⋯ menu off-screen where only a horizontal scroll could
   * reach it. The phone tier has pinned this since #105 and its comment below
   * records the same symptom at ~794px; scoping it to ≤700px left every wider
   * window exposed, which is what surfaced on Cartographer between 701 and
   * 803px (Cinzel's wider measure crosses the viewport there, so it showed up
   * on one theme while the defect was never theme-specific).
   *
   * The chain below is already built for this: .chapter carries min-width: 0
   * with overflow: auto, and .floor-stack min-width: 0 — so the column shrinks
   * to the viewport and a wide floorplan scrolls inside its own pane, which is
   * what the fixed-height two-pane shell wanted all along. */
  grid-template-columns: minmax(0, 1fr);
  height: 100vh;
  max-width: 1600px;
  margin: 0 auto;
}

/* ---- Band 1: the controls-only top bar (#121, §5.2) --------------------- */
/* App verbs ONLY — the single chrome band. No bastion content lives here
 * (crest, name, "held by", figures all moved to the Charter). Reuses the
 * manuscript double-rule band recipe. */

.top-bar {
  display: flex;
  align-items: center;
  /* Wrap is containment, not a layout (#218). Without it the bar's items shrink
   * to min-content before the container gives: the caret wraps INSIDE
   * `Bastions ▾`, the band grows 54→76px, and .app-shell's grid column still
   * blows out — the same min-content failure the phone tier documents below,
   * but sourced from the bar, so the whole DOCUMENT scrolls sideways. With the
   * label nowrap below, the worst case is a two-line band instead. The tablet
   * fold tier should mean this never fires on the default face; it is the guard
   * for wider display faces (Cartographer's Cinzel runs far wider, and
   * per-theme rules may change values, never density) and for save phrases we
   * cannot measure in advance. */
  flex-wrap: wrap;
  gap: 10px;
  margin: 0;
  padding: 9px 20px;
  background:
    linear-gradient(180deg,
        color-mix(in srgb, var(--bb-parchment-light) 55%, transparent) 0%,
        color-mix(in srgb, var(--bb-parchment-mid)   30%, transparent) 100%);
  border-bottom: 1px solid var(--bb-rule);
  box-shadow:
    0 1px 0 var(--bb-parchment),
    0 2px 0 var(--bb-rule-soft);
}

/* Far-left: app identity (NOT bastion identity) — sigil + wordmark. */
.top-bar-brand {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  /* It became a link to home in #264 (it is a <div> only on home itself, where
     it would be a self-link). Without these it draws the UA underline and link
     colour straight through the wordmark. */
  text-decoration: none;
  color: inherit;
  gap: 7px; /* shield sigil's angled flanks add optical space vs the old hexagon */
  margin-right: 4px;
}
.top-bar-brand .brand-sigil {
  display: block;
  flex: 0 0 auto;
  fill: var(--bb-vermillion);
  opacity: 0.9;
}
.top-bar-brand .wordmark {
  font-family: var(--bb-font-display);
  font-size: 1.06rem;         /* 17px */
  font-weight: 600;
  letter-spacing: 0.02em;
  line-height: 1;
  color: var(--bb-ink);
  white-space: nowrap;
}

.top-bar-spacer { flex: 1 1 auto; }

/* Right: app-level controls. */
.top-bar-actions {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  gap: 6px;
  /* Keeps the cluster right-anchored if the wrap guard above ever puts it on
   * its own line — .top-bar-spacer only right-aligns it while they share one. */
  margin-left: auto;
}
.top-bar .btn-ghost {
  background: transparent;
  border: 1px solid transparent;
  box-shadow: none;
  color: var(--bb-ink);
  padding: 6px 14px;
  font-family: var(--bb-font-display);
  font-size: 0.84rem;         /* 13.5px */
  letter-spacing: 0.10em;
  text-transform: uppercase;
  white-space: nowrap;        /* labels are atoms; the ROW wraps, never the label */
}
.top-bar .btn-ghost:hover,
.top-bar .btn-ghost[aria-expanded="true"] {
  background: var(--bb-parchment-mid);
  border-color: var(--bb-rule);
  color: var(--bb-ink);
}
/* Bastions ▾ sits beside the mixed-case wordmark in the left cluster, so it
 * reads in sentence case — the right-cluster app actions stay uppercase. */
#btn-bastions-menu {
  text-transform: none;
  letter-spacing: 0.02em;
  font-size: 0.95rem;
}

/* The autosave note (#save-status / .save-status) carries its own styling; the
 * .top-bar-colophon class only drives the phone-tier hide (§5.2). */
.top-bar-colophon { flex: 0 0 auto; }

/* Band 1 responsive. Thumbs need more air between the right-cluster controls
 * than a mouse does. This lived up in the shared `(pointer: coarse)` block
 * beside the global 44px rule, ABOVE `.top-bar-actions`' own gap — and since a
 * media query adds no specificity and both sit in `components`, source order
 * decided and the 6px desktop gap won on every touch device. It was inert from
 * the day it was written. Down here it is after the rule it overrides, which is
 * the whole reason a surface's responsive rules go last (validate_styles.py G6). */
@media (pointer: coarse) {
  .top-bar-actions { gap: 8px; }
}

/* ---- Band 2: chapter tabs (#121, §5.3) ---------------------------------- */
/* The ledger-tab affordance: tabs are plaques sitting on a recessed band; the
 * active tab shares the page's fill and bridges into it (drops its bottom
 * border), so the row reads as book index tabs, not a menu. Squared (§2.5). */

.chapter-nav {
  display: flex;
  gap: 3px;
  align-items: flex-end;
  padding: 6px 20px 0;
  background: linear-gradient(180deg, var(--bb-parchment-mid), var(--bb-parchment-dark));
}

.chapter-tab {
  position: relative;
  display: inline-flex;
  align-items: baseline;
  gap: 6px;
  background: var(--bb-parchment-mid);
  border: 1px solid var(--bb-rule-soft);
  border-bottom: none;
  border-radius: 0;
  box-shadow: none;
  color: var(--bb-ink-soft);
  padding: 7px 15px;
  margin-bottom: -1px;          /* overlap the page body's top border */
  font-family: var(--bb-font-display);
  font-size: 0.84rem;           /* 13.5px */
  letter-spacing: 0.06em;
  cursor: pointer;
  white-space: nowrap;
  transition: background-color 90ms ease, transform 90ms ease, color 90ms ease;
}
/* Roman ordinal — decorative chapter marker (§5.3 Ruling 1), oldstyle italic. */
.chapter-tab .numeral {
  font-family: var(--bb-font-italic);
  font-style: italic;
  font-size: 0.78rem;
  letter-spacing: 0;
  color: var(--bb-ink-faint);
}
.chapter-tab:hover {
  background: var(--bb-parchment-light);
  color: var(--bb-ink);
  transform: translateY(-1px);
}
.chapter-tab:focus-visible {
  outline: 2px solid var(--bb-vermillion);
  outline-offset: 2px;
}
/* Active — bridges into the page: page-fill, raised, gilt top-edge accent, no
 * bottom rule. Three non-color cues (raise + gilt edge + aria) over the fill. */
.chapter-tab[aria-selected="true"] {
  background: var(--bb-parchment-light);
  border-color: var(--bb-rule);
  border-top: 2px solid var(--bb-gilt);
  padding-top: 6px;             /* compensate the 2px gilt top border */
  color: var(--bb-ink);
  z-index: 2;
}
.chapter-tab[aria-selected="true"] .numeral { color: var(--bb-gilt-deep); }
/* The page-fill strip that masks the page's top border under the active tab —
 * "opens" the page to receive it. */
.chapter-tab[aria-selected="true"]::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: -1px;
  height: 2px;
  background: var(--bb-parchment-light);
}

/* Cross-tab alert dot — 8px danger pip, top-right (§5.3). Mirrored by an
 * aria-label suffix + data-tip set in render-shell.js (non-color channel). */
.chapter-tab.has-alert::before {
  content: "";
  position: absolute;
  top: 4px;
  right: 4px;
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--bb-danger);
}
/* Household vacancy — a gentle gilt pip, not a red alert (#121). */
.chapter-tab.has-vacancy::before {
  content: "";
  position: absolute;
  top: 4px;
  right: 4px;
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--bb-gilt);
}

/* ---- Responsive: the tab band ------------------------------------------- */
/* The nav and the tabs themselves, together, at the end of the band they
 * restyle. Both were in the shared tier block; their base rules are right
 * above, so this is the ordering validate_styles.py G6 asks for. */
@media (max-width: 700px), (max-height: 500px) and (pointer: coarse) {
  /* Chapter-nav: five tabs, horizontally scrollable until the icon slugs land
   * (#121 §5.3 — icons deferred to the icon system). The Roman ordinal drops.
   * Pinned (#158): at the table you switch Register ↔ Chronicle ↔ Grounds
   * mid-turn; the nav earns its band. The top bar still scrolls away. The base
   * parchment gradient is opaque, so nothing bleeds through. z-index clears
   * the floorplan SVG and the (statically positioned but z-indexed) spine
   * (z:5) while staying under the top-bar dropdowns (.bastions-menu chrome,
   * z:40) and popovers (z:200). */
  .chapter-nav {
    position: sticky;
    top: 0;
    z-index: 30;
    padding: 4px 8px 0;
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
  }
  .chapter-tab {
    /* Share the band so all five tabs fit one line (#158 round 2): the long
     * labels shed their articles (.tab-extra) and the tabs flex to fill.
     * overflow-x:auto above stays as the fallback for very narrow screens
     * or wide theme faces. */
    flex: 1 1 auto;
    min-width: 0;
    justify-content: center;
    text-align: center;
    padding: 8px 5px;
    font-size: 0.75rem;
    letter-spacing: 0.02em;
  }
  .chapter-tab .numeral { display: none; }
  .chapter-tab .tab-extra { display: none; }
}

/* ---- Band 3: the page body (#121, §5.4) --------------------------------- */
/* The open page — a bordered parchment card the active tab bridges into. */

.chapter-region {
  min-height: 0;
  overflow: hidden;
  display: grid;
  /* Pin the single cell to the region's own box. Without explicit 1fr tracks
   * the implicit row is content-sized, so a tall chapter (e.g. a long Household)
   * grows past the region and is clipped by overflow:hidden instead of
   * scrolling. 1fr forces the cell to the region height; the chapter scrolls. */
  grid-template-rows: 1fr;
  grid-template-columns: 1fr;
  background: linear-gradient(180deg, var(--bb-parchment-light), var(--bb-parchment) 120px);
  border-top: 1px solid var(--bb-rule);   /* the page frame the active tab opens */
}
.chapter {
  min-height: 0;
  min-width: 0;
  grid-area: 1 / 1;        /* stack all chapters in one cell; hidden ones drop out */
  overflow: auto;
}
/* The hidden attribute must win over the chapters' own display rules —
 * otherwise an inactive chapter would still paint. */
.chapter[hidden] { display: none; }

/* The Grounds manages its own scroll (head + temp + canvas); everything else
 * is a single scrolling column. */
#chapter-grounds { overflow: hidden; }

/* Register / Household / Chronicle are single scrolling columns. */
.chapter-register,
.chapter-household,
.chapter-chronicle {
  padding: 18px 22px 40px;
}

/* ---- Charter (chapter I, §5.4) ------------------------------------------ */
/* The start page as a SPREAD (#301): two leaves of one page over the parchment
 * ground, divided by a hairline down the gutter, with a running colophon across
 * the foot. Verso carries the arms, the circulation, the alarm and the company;
 * recto carries the plan at plate scale and then the story.
 *
 * There is deliberately NO sheet — no inset frame, no drop shadow, no page
 * edges floating on the ground. The spread STRUCTURE is native (the Chronicle
 * and the Register are both two columns on the ground, divided by a hairline);
 * a drawn page object is not, and it also draws a box around the layout's empty
 * area and gives it a bottom edge, which is exactly what makes the Register's
 * very empty leaf column read as fine and would have made this one read as
 * broken. */
.chapter-charter { padding: 26px 24px 56px; }
.charter-spread {
  max-width: 1360px;
  margin: 0 auto;
  display: grid;
  grid-template-columns: 1fr 1fr;
}
/* min-width:0 is load-bearing, not tidiness: a grid item's automatic minimum is
 * min-content, and the colophon's nowrap figures would otherwise push the
 * spread past the viewport rather than wrapping inside it. */
.charter-leaf {
  min-width: 0;
  padding: 8px 40px 26px;
  display: flex;
  flex-direction: column;
  gap: 18px;
  align-items: stretch;
}
.charter-recto { border-left: 1px solid var(--bb-rule); }

/* ---- verso head: the arms in a cartouche -------------------------------- */
/* The crest is the largest thing on the page on purpose — it is the one piece
 * of art the player brought, so it gets a proper cartouche: a closed double
 * gilt rule, drawn in CSS, nothing here is an image.
 *
 * It began as two gilt rules plus two inset hairline pales, which formed an
 * almost-closed rectangle — horizontals full-width, verticals inset and short —
 * and read as a misaligned box rather than a device. A frame is either complete
 * or absent; a three-quarters one always looks like a mistake.
 *
 * And it is ABSENT until there is art to frame. Empty, the slot already draws
 * its own dashed ghost border, so the cartouche made three concentric
 * rectangles around a placeholder on day one — the state this chapter is most
 * responsible for getting right. `has-crest` is set by syncCrestUI, so the
 * frame follows the art with nothing new to keep in sync. */
.charter-cartouche {
  align-self: center;
  position: relative;
  padding: 15px 18px 13px;
}
.charter-identity.has-crest .charter-cartouche { border: 1px solid var(--bb-gilt); }
.charter-identity.has-crest .charter-cartouche::before {
  content: "";
  position: absolute;
  top: 4px;
  right: 4px;
  bottom: 4px;
  left: 4px;
  border: 1px solid var(--bb-gilt);
  opacity: 0.42;
}
.charter-identity {
  display: flex;
  flex-direction: column;
  align-items: stretch;
}
/* Overrides the rail's 64px sizing. */
.charter-identity .charter-crest-wrap {
  position: relative;
  width: 200px;
}
.charter-identity .crest-slot {
  width: 200px;
  height: 240px;                /* 5:6 heater-shield ratio */
  padding: 3px;
}
/* Crest menu opens DOWN under the seal (overriding the rail's open-up). */
.charter-identity .crest-menu {
  top: calc(100% + 4px);
  bottom: auto;
  left: 0;
  right: auto;
}
.charter-identity .bastion-name-wrap {
  position: relative;
  width: 100%;
}
/* Unconditional illuminated drop-cap (§5.4). The versal does NOT float here —
 * the masthead is centred under the cartouche, and a floated initial on a
 * centred line drops out of the optical centre and reads as a typo. */
.charter-identity .bastion-name-display::first-letter {
  color: var(--bb-vermillion);
  /* #218 — see .house-name-display::first-letter: uppercase keeps a lowercase
   * initial from reading as a dropped-to-second-line glyph. */
  text-transform: uppercase;
}
.charter-identity .bastion-name-display {
  display: block;
  font-size: 2.5rem;            /* 40px — the cover-page title */
  line-height: 1.1;
  text-align: center;
}
.charter-identity .bastion-name {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  min-width: 0;
  font-family: var(--bb-font-display);
  font-size: 2.5rem;            /* matches the overlay */
  font-weight: 500;
  letter-spacing: 0.01em;
  text-align: center;
  color: transparent;           /* read state shows the overlay */
  caret-color: transparent;
  background: transparent;
  border: 1px solid transparent;
  padding: 12px 8px 0;
  box-shadow: none;
}
.charter-identity .bastion-name:focus {
  outline: none;
  color: var(--bb-ink);
  caret-color: var(--bb-vermillion);
  background: var(--bb-parchment-light);
  border-color: var(--bb-vermillion);
  box-shadow: 0 0 0 2px rgba(168,40,29,0.20);
}
.charter-identity .bastion-name-wrap:focus-within .bastion-name-display {
  visibility: hidden;
}
.charter-held-by {
  font-family: var(--bb-font-italic);
  font-style: italic;
  font-size: 1.02rem;
  color: var(--bb-ink-soft);
  line-height: 1.3;
  text-align: center;
  margin-top: -10px;
}
.charter-held-by:empty { display: none; }
.charter-identity .bastion-type-badge { align-self: center; }
.charter-identity .charter-acts {
  justify-content: center;
  gap: 6px 22px;
  margin-top: -6px;
}

/* ---- verso body: the alarm, then the roll ------------------------------- */
.charter-identity .charter-compliance { margin: 0; padding: 12px 15px 10px; }

/* The Charter alarm's phone-tier DOOR (#218) — built by paintComplianceHost for
 * the charter host only. Declared HERE, beside the surface it belongs to and
 * above the tier that turns it on: the base rule is `display: none`, so had it
 * stayed with the shared `.compliance-notice` rules (which sit far below this
 * section) it would have come after the phone block and silently won, leaving
 * the door invisible forever. validate_styles.py gate G6 caught exactly that.
 * The banner paints as it always has at every wider measure. */
.compliance-door { display: none; }
/* The Charter's breach shape is the whole SENTENCE, and the sentence is the
 * door (#300 settled the shape, #301 made it a control). It must not look like
 * a button: it is a line of text you can click, so it inherits the item's
 * type and takes an underline only on hover/focus. */
.compliance-sentence {
  background: none;
  border: 0;
  box-shadow: none;
  padding: 0;
  margin: 0;
  font: inherit;
  color: inherit;
  letter-spacing: inherit;
  text-transform: none;
  text-align: left;
  cursor: pointer;
}
.compliance-sentence:hover { text-decoration: underline dotted; text-underline-offset: 3px; }
.compliance-sentence:focus-visible { outline: 2px solid var(--bb-vermillion); outline-offset: 2px; }

/* The roll is ruled to the foot of the leaf (#301). The verso's mass is
 * contingent — compliance only when something is broken, the roll only with the
 * party — while the recto's is fixed, so a healthy solo bastion measured 372px
 * of dead verso at 1440. flex:1 on the roll puts that space INSIDE it and
 * .owners-rule draws it as ruled paper: room for more hands rather than a page
 * that ran out. It collapses to nothing on its own once the party fills the
 * leaf, which is why there is no state to compute anywhere in JS.
 *
 * The mask is not decoration-of-decoration: without it the ruling ends in a
 * hard-edged block of empty slots that reads as a roster you are failing to
 * fill, and its last rule collides with the colophon's. */
.charter-identity .charter-owners {
  flex: 1 1 auto;
  min-height: 0;
  display: flex;
  flex-direction: column;
  margin: 0;
}
.charter-identity .owner-grid { flex: 0 0 auto; display: block; border-top: 1px solid var(--bb-rule); }
/* The roll's own first rule is the one under OWNERS — the section label's
   border would be a second one two pixels above it, which is the double
   divider #301 flagged. The rule belongs to the roll, not the label. */
.charter-identity .owners-label { border-bottom: 0; padding-bottom: 0; margin-bottom: 6px; }
/* Three lines per hand — name over class over standing, verbs on the rule —
   rather than a boxed card per owner: inside a ruled roll a bordered card is a
   second frame, and the roll's rules already separate the hands.
   display:contents lifts the portrait and the id block into the same grid, so
   the class input keeps the full measure and never clips its own value. */
.charter-identity .owner-card {
  background: none;
  border: 0;
  border-bottom: 1px solid var(--bb-rule-soft);
  display: grid;
  grid-template-columns: 46px minmax(0, 1fr);
  column-gap: 13px;
  row-gap: 5px;
  padding: 12px 2px 11px;
}
.charter-identity .owner-head { display: contents; }
.charter-identity .owner-portrait { width: 46px; height: 46px; }
.charter-identity .owner-id { min-width: 0; display: flex; flex-direction: column; gap: 3px; }
.charter-identity .owner-meta { display: flex; align-items: center; gap: 14px; flex-wrap: nowrap; }
.charter-identity .owner-class { min-width: 0; flex: 1 1 auto; }
.charter-identity .owner-standing,
.charter-identity .owner-foot { grid-column: 2; }
/* The credential group's own label is a heading over one row of chips inside a
   roll that is already headed "Owners" — it says nothing the chips don't. */
.charter-identity .cred-group-label { display: none; }
.charter-identity .owner-standing { flex-direction: row; flex-wrap: wrap; align-items: center; gap: 6px; }
.charter-identity .cred-group { flex-direction: row; flex-wrap: wrap; gap: 6px; }
.charter-identity .owner-foot { display: flex; gap: 18px; justify-content: flex-end; white-space: nowrap; }
/* Coarse pointers keep the 44px floor — the roll is a list of controls. */
.charter-identity button.owner-card.owner-add {
  display: flex;
  min-height: 44px;
  border: 0;
  border-bottom: 1px solid var(--bb-rule-soft);
  justify-content: flex-start;
  padding-left: 2px;
}
.charter-identity .party-name-field { max-width: 300px; }
.owners-rule {
  flex: 1 1 auto;
  min-height: 0;
  background-image: repeating-linear-gradient(to bottom,
    transparent 0 45px, var(--bb-rule-soft) 45px 46px);
  -webkit-mask-image: linear-gradient(to bottom, #000 0, #000 38%, transparent 96%);
  mask-image: linear-gradient(to bottom, #000 0, #000 38%, transparent 96%);
}

/* ---- recto head: the plate ---------------------------------------------- */
.charter-plate {
  margin: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 12px;
}
/* The cut is a real button (#301: no click handlers on regions). A large silent
 * clickable area is a dead control under #145, so it carries a visible hover
 * and focus affordance — the corner ticks brighten and the ground lifts. */
.charter-plate-cut {
  position: relative;
  width: fit-content;
  max-width: 100%;
  padding: 14px;
  display: grid;
  place-items: center;
  background: var(--bb-parchment);
  border: 0;
  box-shadow: none;
  cursor: pointer;
  transition: background-color 120ms ease;
}
.charter-plate-cut::before,
.charter-plate-cut::after {
  content: "";
  position: absolute;
  width: 14px;
  height: 14px;
  border: 1px solid var(--bb-rule);
  transition: border-color 120ms ease;
}
.charter-plate-cut::before { top: 5px; left: 5px; border-right: 0; border-bottom: 0; }
.charter-plate-cut::after { bottom: 5px; right: 5px; border-left: 0; border-top: 0; }
.charter-plate-cut:hover { background: var(--bb-parchment-light); }
.charter-plate-cut:hover::before,
.charter-plate-cut:hover::after { border-color: var(--bb-gilt); }
.charter-plate-cut:focus-visible { outline: 2px solid var(--bb-vermillion); outline-offset: 2px; }
.charter-plate-cut svg { display: block; grid-area: 1 / 1; max-width: 100%; height: auto; }
/* Day one: #302 returns a ruled sheet rather than null, so the plate has a
 * survey to letter over instead of no artifact at all. */
.charter-plate-blank {
  grid-area: 1 / 1;
  font-family: var(--bb-font-italic);
  font-style: italic;
  font-size: 1rem;
  color: var(--bb-ink-faint);
}
/* `max-width` is load-bearing (#218). The caption is a shrink-to-fit item in a
   centred column flex, so its width follows its widest content — fine while
   every line was app-authored and short, and not fine once it carries a
   PUBLISHER's founding date. A homebrew month name may be 80 characters, which
   sized this box to 667px inside a 366px column: overflowing, and clipped
   rather than scrolled, which is exactly how the calendar block's collapse hid
   itself for a release. Bounded here, the date wraps inside the plate instead. */
.charter-plate figcaption { text-align: center; line-height: 1.45; max-width: 100%; }
.charter-plate figcaption b {
  display: block;
  font-family: var(--bb-font-display);
  font-weight: 500;
  font-size: 0.78rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--bb-ink-faint);
  margin-bottom: 4px;
}
.charter-plate figcaption span {
  font-family: var(--bb-font-italic);
  font-style: italic;
  font-size: 1rem;
  color: var(--bb-ink-soft);
}
/* THE FOUNDING, for a reader of a shared bastion only (#218). A third register
   under the caption — a short gilt rule, the label in the caption's own voice,
   the date in the display face: a cornerstone inscription rather than a row in
   a dashboard. It bends two things deliberately. A cartographic dateline
   conventionally dates the SURVEY rather than the ground, which is what the
   word "Founded" is there to disambiguate; and it takes the caption from two
   registers to three, with a rule inside a caption.
   No tier rule needed — verified at 390px in both date lengths, where it wraps
   to two centred lines. Its own [hidden] guard because the block display would
   otherwise beat the UA rule (the house pattern here — see .chapter[hidden]). */
.charter-plate figcaption .plate-founded {
  display: block;
  position: relative;
  margin-top: 10px;
  padding-top: 10px;
}
.charter-plate figcaption .plate-founded[hidden] { display: none; }
.charter-plate figcaption .plate-founded::before {
  content: "";
  position: absolute;
  top: 0;
  left: 50%;
  width: 46px;
  margin-left: -23px;
  border-top: 1px solid var(--bb-gilt);
}
/* Matches the `figcaption b` beside it exactly, one step smaller. */
.charter-plate figcaption .pf-w {
  display: block;
  margin-bottom: 2px;
  font-family: var(--bb-font-display);
  font-style: normal;
  font-weight: 500;
  font-size: 0.72rem;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--bb-ink-faint);
}
.charter-plate figcaption .pf-d {
  display: block;
  font-family: var(--bb-font-display);
  font-style: normal;
  font-weight: 500;
  font-size: 1rem;
  letter-spacing: 0.04em;
  color: var(--bb-ink);
  /* A month name is whatever a homebrew author typed, and nothing obliges it to
     contain a space. Wrapping at spaces alone leaves an 80-character single word
     overflowing the bound above — so break the run instead. Breaking a word is
     ugly; losing the end of a date to a clip is wrong. */
  overflow-wrap: break-word;
}

/* ---- recto body: the story ---------------------------------------------- */
.charter-lore { display: flex; flex-direction: column; margin: 6px 0 0; }
/* The Works / Stores keep the label-shares-a-row-with-an-action shape; the
 * Charter's lore head no longer has an action to share it with (#301 deleted
 * the Edit button — #lore-view already binds click and Enter/Space to the same
 * enterEdit), so it is just a labelled rule now and inherits the rest. */
.charter-lore-head,
.chronicle-works-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
  flex-wrap: wrap;
  margin: 22px 0 10px;
  padding-bottom: 4px;
  border-bottom: 1px solid var(--bb-rule);
}
.charter-lore-head .section-label,
.chronicle-works-head .section-label {
  margin: 0;
  padding: 0;
  border-bottom: 0;
}
/* Rendered read view — beautiful, auto-height prose; click anywhere to edit. */
.lore-view {
  width: 100%;
  min-height: 3.2em;
  padding: 10px 12px;
  font-family: var(--bb-font-text);
  font-size: 1.04rem;
  line-height: 1.6;
  color: var(--bb-ink);
  white-space: pre-wrap;
  word-break: break-word;
  cursor: text;
  border: 1px solid transparent;
  border-left: 2px solid var(--bb-rule-soft);
  transition: background-color 90ms ease, border-color 90ms ease;
}
.charter-lore .lore-view::first-letter {
  font-family: var(--bb-font-display);
  font-size: 2.6rem;
  line-height: 0.86;
  color: var(--bb-vermillion);
  float: left;
  margin: 4px 8px 0 0;
  text-transform: uppercase;
}
/* :empty has no first letter to raise, and the prompt is a ::before — so the
 * versal must not apply to the empty state's italic marginalia. */
.charter-lore .lore-view:empty::first-letter {
  float: none;
  font-family: inherit;
  font-size: inherit;
  line-height: inherit;
  color: inherit;
  margin: 0;
}
.lore-view:hover {
  background: var(--bb-parchment);
  border-left-color: var(--bb-gilt);
}
.lore-view:focus-visible {
  outline: 2px solid var(--bb-vermillion);
  outline-offset: 2px;
}
/* Empty state — italic marginalia prompt (content from data-empty). */
.lore-view:empty::before {
  content: attr(data-empty);
  font-family: var(--bb-font-italic);
  font-style: italic;
  color: var(--bb-ink-faint);
}
.charter-lore .notes-field {
  width: 100%;
  min-height: 8em;
  overflow: hidden;          /* JS auto-sizes height to content */
}

/* ---- the colophon: the whole survey, across the foot -------------------- */
.charter-colophon {
  grid-column: 1 / -1;
  padding: 14px 40px 0;
  border-top: 1px solid var(--bb-rule);
}
.charter-survey:empty { display: none; }
.sv-lead {
  margin: 0 0 8px;
  font-family: var(--bb-font-italic);
  font-style: italic;
  font-size: 1.02rem;
  color: var(--bb-ink-soft);
}
/* The verb floats right so it sits on the survey's first line and the figures
 * wrap around it — a right-anchored flex row would grow leftward instead and
 * push the figures off their own line. */
.sv-row {
  line-height: 1.95;
  min-width: 0;
}
.sv-row > .sv-verb { float: right; margin-left: 20px; }
.sv-cell {
  display: inline;
  white-space: nowrap;
  margin-right: 4px;
  background: none;
  border: 0;
  box-shadow: none;
  padding: 0;
  font: inherit;
  letter-spacing: inherit;
  text-transform: none;
  color: inherit;
}
/* The separator is a TRAILING mark inside the preceding cell's own nowrap box,
 * never a leading one on the next. A `::before` cannot be kept off the start of
 * a line — every wrap would open with an orphaned interpunct, which is exactly
 * what the survey does at phone width, where each figure takes its own line. */
/* Fires only when a VISIBLE cell or follow-wrapper actually comes after, which
 * `:not(:last-child)` did not test. `.sv-circulation` carries `.sv-row` too, so
 * `.sv-row > .sv-verb { float: right }` above pulls "Share a fresh link…" to the
 * right margin while leaving it a following sibling in the DOM — so the Last
 * shared cell was never `:last-child` and trailed a stranded interpunct into
 * ~980px of nothing, in every owner configuration including no publish block at
 * all. `~`, not `+`: the floated verb sits between the two cells. The
 * `:not([hidden])` matters because `wireAvailable()` hides `.sv-follow`
 * asynchronously and a `[hidden]` sibling still matches a combinator. */
.sv-cell:has(~ .sv-cell:not([hidden]))::after,
.sv-cell:has(~ .sv-follow:not([hidden]))::after {
  content: " ·";
  margin-left: 6px;
  color: var(--bb-ink-faint);
}
.sv-l {
  font-family: var(--bb-font-display);
  font-size: 0.76rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--bb-ink-faint);
  margin-right: 6px;
}
.sv-v {
  font-family: var(--bb-font-display);
  font-size: 1.06rem;
  color: var(--bb-ink);
}
.sv-u {
  font-family: var(--bb-font-italic);
  font-style: italic;
  font-size: 0.92rem;
  color: var(--bb-ink-soft);
  margin-left: 5px;
}
/* Over cap and nothing-yet: the WORDS carry both states (capPhrase writes
 * "Sir Reginald over by 7", never a colour alone, §4) — the ink is a second
 * channel on a fact the sentence already makes plain. */
.sv-cell.is-over .sv-v,
.sv-cell.is-over .sv-u { color: var(--bb-vermillion-deep); }
.sv-cell.is-nil .sv-v { font-family: var(--bb-font-italic); font-style: italic; color: var(--bb-ink-soft); }
button.sv-cell { cursor: pointer; }
button.sv-cell:hover .sv-v { text-decoration: underline dotted; text-underline-offset: 3px; }
button.sv-cell:hover .sv-l { color: var(--bb-ink-soft); }
button.sv-cell:focus-visible { outline: 2px solid var(--bb-vermillion); outline-offset: 2px; }
.sv-circulation { margin-top: 2px; }
.sv-follow { white-space: nowrap; }
.sv-verb {
  background: none;
  border: 0;
  box-shadow: none;
  padding: 0;
  cursor: pointer;
  font-family: var(--bb-font-italic);
  font-style: italic;
  font-size: 0.9rem;
  letter-spacing: normal;
  text-transform: none;
  color: var(--bb-gilt-deep);
  text-decoration: underline dotted;
  text-underline-offset: 2px;
  white-space: nowrap;
}
.sv-verb:hover { color: var(--bb-ink); }
.sv-verb:focus-visible { outline: 2px solid var(--bb-gilt); outline-offset: 1px; }
.sv-verb-inline { margin-left: 10px; }

/* The lending slip (#304) — the colophon's record of who holds a copy of this
 * bastion. Relay deployments only: render-shell.js resolves wireAvailable()
 * after the paint and shows either this or the inline .sv-circulation row,
 * never both, because this plate ABSORBS "Last shared".
 *
 * It takes the colophon's FULL MEASURE, and that is the fix rather than a
 * flourish: at its first drawn width (340px in a 1280px band) it was an
 * anomalous narrow column with ~940px of void beside it, and flowing the names
 * inside that column shortened it without touching the void. At full measure it
 * joins the band's rhythm and reads quieter, not louder — which is also what
 * keeps it a footnote to the #302 plate rather than a rival to it. */
.circ {
  margin-top: 6px;
  padding-top: 8px;
  border-top: 1px solid var(--bb-rule);
}
/* The whole head line is the control, never a caret beside a label: under the
 * global coarse-pointer floor a caret-only button measures 13×44px — the floor
 * gives it height and no width — and reads as an orphan. */
.circ-head {
  display: flex;
  align-items: baseline;
  gap: 6px;
  width: 100%;
  background: none;
  border: 0;
  box-shadow: none;
  padding: 0;
  cursor: pointer;
  text-align: left;
}
.circ-caret { color: var(--bb-ink-faint); font-size: 0.7rem; }
.circ-title {
  font-family: var(--bb-font-display);
  font-size: 0.66rem;
  letter-spacing: 0.15em;
  text-transform: uppercase;
  color: var(--bb-ink-faint);
}
.circ-summary {
  font-family: var(--bb-font-italic);
  font-style: italic;
  font-size: 0.82rem;
  color: var(--bb-ink-soft);
}
.circ-head:focus-visible { outline: 2px solid var(--bb-gilt); outline-offset: 2px; }
.circ-body { margin-top: 6px; }
.circ-facts { display: flex; align-items: baseline; gap: 26px; flex-wrap: wrap; }
.circ-verbs { margin-left: auto; display: flex; gap: 18px; }
.circ-t {
  font-family: var(--bb-font-italic);
  font-style: italic;
  font-size: 0.82rem;
  color: var(--bb-ink-soft);
  margin-right: 8px;
}
.circ-d { font-size: 0.88rem; color: var(--bb-ink); }
.circ-roll { display: flex; align-items: baseline; margin-top: 4px; }
.circ-roll > .circ-t { flex: 0 0 auto; }
/* column-count is set inline per roll: fixed at four, a three-name roster draws
 * three names at the left end of three rules spanning emptiness, which reads as
 * an unfilled form rather than a record. */
.circ-slots { flex: 1 1 auto; min-width: 0; column-gap: 26px; }
.circ-slot {
  display: block;
  font-size: 0.88rem;
  color: var(--bb-ink);
  padding: 2px 0;
  /* border-BOTTOM, never `+ { border-top }`: in a multi-column block the latter
   * leaves column 1 with no rule above its first name and every other column
   * with one above theirs. */
  border-bottom: 1px dotted var(--bb-rule-soft);
  break-inside: avoid;
}
.circ-slot.is-asked { color: var(--bb-ink-soft); }
.circ-none {
  font-family: var(--bb-font-italic);
  font-style: italic;
  color: var(--bb-ink-soft);
}

/* Sourcebooks: per-bastion configuration, so it sits last in the colophon at
 * the smallest weight on the page. Label and closed-state summary share the
 * summary row rather than being separated across the whole measure, so they
 * read as one control. */
.charter-colophon .charter-sources { margin-top: 10px; border-top: 1px solid var(--bb-rule-soft); }
.charter-colophon .charter-sources > summary {
  display: flex;
  align-items: baseline;
  gap: 10px;
  padding: 9px 0;
}
.charter-colophon .charter-sources-summary { margin-left: 0; }

/* ---- Charter · Responsive ----------------------------------------------- */
/* LAST in this section, after every base rule above: a media query adds no
 * specificity, so a tier rule and the unconditional rule it overrides are
 * decided purely by source order.
 *
 * These are SINGLE-SURFACE rules and so they live here, beside the surface,
 * rather than in the phone-tier cross-surface band — which is where the old
 * Charter block sat, and which is the placement mistake to avoid. The band
 * keeps only what genuinely coordinates several components at once. */

/* One column well before the phone tier: two 1fr leaves stop being two leaves
 * long before 700px, and the plate is the first thing to suffer. 980px is the
 * Charter's own established fold — no new breakpoint. */
@media (max-width: 980px) {
  .charter-spread { grid-template-columns: 1fr; }
  .charter-recto { border-left: 0; border-top: 1px solid var(--bb-rule); padding-top: 22px; }
  /* No second leaf to balance against, so the roll stops being ruled to a foot
   * that no longer means anything. */
  .owners-rule { display: none; }
  .charter-plate-cut svg { max-width: 340px; }
}

/* Phone tier (#158/#218, re-derived for the spread in #301, and again for the
 * masthead in a second #218 pass).
 *
 * The technique has survived three rewrites because it is the right one:
 * flatten both leaves and the colophon into ONE grid, so every part can be
 * ordered against every other, rather than reordering three containers that
 * each hold half of what a phone reader needs first.
 *
 * WHAT ARRIVAL NEEDS. On a phone the Charter is a TITLE PAGE, not a dashboard:
 * the arms, the name and who holds them as one masthead; the ALARM AS A DOOR
 * (a 44px struck line, see .compliance-door, so a 313px banner cannot decide
 * where the fold falls); the plan at its full measure with its own account
 * engraved beneath it; then the roll, the story, the circulation verbs and the
 * colophon.
 *
 * THE MASTHEAD (#218). #301 pass 2 made the crest the page's largest element;
 * #218 reassigned that to the plan and left the arms and "held by …" stranded
 * below the fold as a bookplate — which reads as two half-mastheads rather than
 * one, since the name went on leading the page alone. They are one block now,
 * subordinate to the plan in SIZE (a 64px shield, not 200) and ahead of it in
 * ORDER.
 *
 * THE SURVEY IS NOT ON THIS TIER, but for two rows. Every figure on it is a
 * digest of another chapter and is already somewhere the reader can see: `Turn
 * N` is the permanent top-bar cartouche, `Specials n/m` is echoed by `.cap-echo`
 * on both the Grounds library and the Register header, an over-cap breach is a
 * sentence inside the alarm door, Treasury is a section of the Chronicle, and
 * Chambers / Garrison / Household / Walls are each the headline of the chapter
 * they belong to. Stacked at this measure they cost ~360px — more than half the
 * viewport — for facts the reader is one tab away from.
 *
 * So the editorial rule is: THE LEAF CARRIES WHAT THE GROUND IS AND WHAT IT
 * COST. Chambers, floors and walls letter in the plan's own legend
 * (#charter-plate-cap, unhidden below); Outlay and The works stay, as two
 * ledger rows, because those two are the only figures with no other home on a
 * phone at all. The day-one sentence and the circulation record stay too: one
 * is an invitation and the other is a fact about this document, and neither is
 * a digest of anything.
 *
 * Trade-off, unchanged from #158: keyboard order stays DOM order, so tab order
 * diverges from visual order for the reordered blocks. Visual-only reflow, the
 * same bargain `order` makes everywhere else in this sheet. The masthead is the
 * one part that costs nothing here — its three members are already contiguous
 * in the verso, in this order.
 *
 * WIDTH-ONLY, NOT THE TIER (#218, second landscape pass). This block used to
 * ride PHONE_TIER_QUERY whole, and its second branch — `(max-height: 500px)
 * and (pointer: coarse)`, a phone on its side — ran a layout authored for a
 * 369px measure at ~828px. Measured at 852×393: the top bar and the chapter
 * band cost 123px, the fold is ~372px, and no ordering of these blocks clears
 * one complete one — the alarm door began at y=374, a pixel from the fold —
 * while the plate stood in a single left-hugging column in a wide field.
 * Every rule here makes the page TALLER (one column, a masthead, the plate at
 * full measure), which is exactly the class #283's ruling keeps off the
 * tier's short-viewport branch. So it takes the width half alone, and a
 * landscape phone falls through to the `≤980px` one-column spread above,
 * which was authored for a wide short column and reads as one. Nothing here
 * is a tap-target or chrome rule that the landscape branch would miss: the
 * door's 44px box goes with the door, and the base banner returns in its
 * place. */
@media (max-width: 700px) {
  .chapter-charter { padding: 14px 12px 40px; }
  .charter-spread {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr);
    align-items: center;
    gap: 8px 14px;
  }
  .charter-leaf,
  .charter-colophon { display: contents; }

  /* ---- THE MASTHEAD (#218) ----
   * The arms, the name and who holds them are ONE block in column order: the
   * shield at the left spanning both lines, the name on the first, "held by"
   * on the second. DOM order in the verso is already cartouche → name-wrap ->
   * held-by, so this costs nothing in tab order and auto-placement needs no
   * `dense`.
   *
   * 64px, not the 84 the retired bookplate used and not the desktop 200. The
   * deciding state is crest-ABSENT, which is permanent for most bastions: at 64
   * the dashed ghost shield reads as a blank escutcheon on a title page, at 84
   * as an upload dropzone at the top of the page forever. It also buys the name
   * a 288px track instead of 268, which the ladder below spends.
   *
   * Selectors matched to the base rules' specificity on purpose — the cartouche
   * frame is `.charter-identity.has-crest .charter-cartouche` (0,2,1), so a bare
   * `.charter-cartouche { border: 0 }` here would silently lose. A media query
   * adds no specificity; only source order and weight decide. */
  .charter-cartouche {
    order: -7;
    grid-column: 1;
    grid-row: span 2;
    align-self: start;
    margin: 0;
    padding: 0;
  }
  /* At this size a double rule at a 4px inset is two hairlines two pixels
   * apart, which reads as a printing fault rather than a frame. */
  .charter-identity.has-crest .charter-cartouche { border: 0; }
  .charter-identity.has-crest .charter-cartouche::before { content: none; }
  .charter-identity .charter-crest-wrap,
  .charter-identity .crest-slot { width: 64px; }
  .charter-identity .crest-slot { height: 77px; padding: 2px; }

  .charter-identity .bastion-name-wrap {
    order: -7;
    grid-column: 2;
    grid-row: 1;
    align-self: end;
  }
  .charter-identity .bastion-name-display,
  .charter-identity .bastion-name { font-size: 1.9rem; text-align: left; }
  /* The overlay must match the input's TEXT ORIGIN, not its box. The input
   * carries a 1px transparent border in both states, so the overlay takes 1px
   * of padding where the input takes none — the shared base pairs them as
   * `12px 9px 0` against `12px 8px 0` + that border, and overriding one half
   * alone makes the title jump 9px left and 12px up the moment the reader taps
   * to rename it. That is the likeliest first act on a phone and it is
   * invisible in a static render, which is why both halves move together here.
   *
   * The clamp is the backstop: 5.2em clears FOUR lines at the 1.3rem step,
   * which is where an 80-character name lands — measured, because 4.4em clipped
   * it by 7px and the clip is silent. Past the enforced bound it clamps again,
   * which is the job. `min-height` goes because the base 56px is room for a
   * 2.5rem drop cap; nothing here is set at 2.5rem. */
  .charter-identity .bastion-name { padding: 0; }
  .charter-identity .bastion-name-display {
    padding: 1px;
    min-height: 0;
    max-height: calc(6px + 5.2em);
  }
  /* THE TITLE LADDER (#218). A printed title page sets a long name smaller, and
   * so does this one. At a fixed 1.9rem the 80-character schema bound overruns
   * the clamp by 121px, and that clip is SILENT: .bastion-name is inset:0 over a
   * pointer-events:none overlay, so elementFromPoint hands back the input and a
   * data-tip can never fire. Measured at the bound: four lines at 1.3rem, a
   * 127px masthead, nothing lost.
   *
   * `data-len` is written by syncBastionNameDisplay as a BUCKET — the buckets
   * ARE the type steps, and a rule per length is not a thing. A size that
   * depends on content is new to this sheet; it is confined to this tier and
   * this one element. */
  .charter-identity[data-len="mid"] .bastion-name-display,
  .charter-identity[data-len="mid"] .bastion-name { font-size: 1.55rem; }
  .charter-identity[data-len="long"] .bastion-name-display,
  .charter-identity[data-len="long"] .bastion-name { font-size: 1.3rem; }

  .charter-held-by {
    order: -7;
    grid-column: 2;
    grid-row: 2;
    align-self: start;
    text-align: left;
    margin: 2px 0 0;
    font-size: 0.9rem;
  }
  /* held-by empty is a real state — `.charter-held-by:empty { display: none }`
   * in the base section — and then the shield spans a row with nothing in it,
   * which collapses on its own: `grid-row: span 2` over one populated row is one
   * row. The name then centres against the shield rather than sitting low. */
  .charter-identity:has(.charter-held-by:empty) .bastion-name-wrap { align-self: center; }
  .charter-identity .bastion-type-badge { justify-self: start; }

  /* Everything outside the bookplate row takes the full measure, in reading
   * order. `.charter-acts` joins them now: the circulation verbs used to be the
   * THIRD block on the page, so an outbound export outranked the alarm, the
   * figures, the plan, the roll and the story — on the device where sharing is
   * the rarest act. They read as a foot, under a hairline, with the colophon. */
  .charter-identity .bastion-type-badge,
  .charter-identity .charter-compliance,
  .charter-identity .charter-acts,
  .charter-plate,
  .charter-identity .charter-owners,
  .charter-lore,
  .charter-colophon .charter-sources,
  .charter-colophon .charter-calendar,
  .charter-survey { grid-column: 1 / -1; min-width: 0; }
  /* Selector matched to the base rule `.charter-identity .charter-compliance`
     (0,2,0) — a bare `.charter-compliance { padding }` here is (0,1,0) and
     loses outright, leaving the closed door in a 68px box instead of a 46px
     one. That failure is invisible: the door looks right and simply carries
     22px of the base banner's padding it no longer needs. */
  .charter-identity .charter-compliance { order: -5; margin-top: 10px; padding: 0 10px; }
  .charter-identity .charter-compliance[data-open] { padding-bottom: 10px; }
  .charter-plate { order: -4; margin-top: 10px; width: 100%; gap: 9px; }
  /* The survey is ruled to the plan ABOVE it rather than off the block before
   * it — hence border-bottom, not -top. It still sits directly under the plan;
   * what the masthead pass changed is that nearly all of it is gone, so the rule now closes
   * off the plan and its two-row account together. */
  .charter-survey {
    order: -3;
    margin: 2px 0 12px;
    padding: 0 0 10px;
    border-top: 0;
    border-bottom: 1px solid var(--bb-rule);
  }
  /* A survey whose every row is display:none is still a box, and it would paint
   * that border-bottom over nothing. Guard on CONTENT, not on the element:
   * `:empty` is false here — the children exist and are hidden. All four things
   * that can legitimately show are named, so the guard fails OPEN. */
  .charter-survey:not(:has(.sv-lead)):not(:has(.sv-cell[data-fig="outlay"])):not(:has(.sv-cell[data-fig="works"])):not(:has(.sv-circulation:not([hidden]))):not(:has(.circ:not([hidden]))) {
    display: none;
  }
  .charter-identity .bastion-type-badge { order: -1; }
  .charter-identity .charter-owners { order: 0; margin-top: 14px; }
  .charter-lore { order: 1; }
  .charter-identity .charter-acts {
    order: 2;
    justify-self: start;
    align-self: start;
    justify-content: flex-start;
    margin: 12px 0 0;
    padding-top: 10px;
    border-top: 1px solid var(--bb-rule);
  }
  .charter-colophon .charter-sources { order: 3; }
  .charter-colophon .charter-calendar { order: 4; }

  .charter-recto { border: 0; padding: 0; }
  /* The plan runs to its FULL measure: PLATE_PX = 388 is the seal's intrinsic
   * width, so `max-width: 100%` never upscales — it reads 366px at 390 and
   * never more than 388. The 320/230 caps this tier used
   * to carry existed to leave room for the account beneath; the account is two
   * rows now, and the plan is the frontispiece. */
  .charter-plate-cut svg { max-width: 100%; }

  /* THE ALARM AS A DOOR — a struck line standing in for a 313px banner, so the
     alarm keeps its place at the top without deciding where the fold falls.
     GILT, matching `.compliance-notice` exactly: the door stands in for that
     banner, and vermillion would escalate what the alarm claims rather than
     restate it. Closed, it IS the banner and carries its own 44px box; open, it
     stays as the heading and `.compliance-head` stands down as a duplicate. */
  .compliance-door {
    display: flex;
    align-items: center;
    gap: 8px;
    width: 100%;
    min-height: 44px;
    padding: 0;
    background: none;
    border: 0;
    box-shadow: none;
    cursor: pointer;
    text-align: left;
    font-family: var(--bb-font-display);
    font-size: 0.85rem;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--bb-gilt-deep);
  }
  .compliance-door-n {
    margin-left: auto;
    font-family: var(--bb-font-italic);
    font-style: italic;
    font-size: 0.8rem;
    letter-spacing: 0.02em;
    text-transform: none;
    color: var(--bb-ink-soft);
  }
  .compliance-door-caret { font-size: 0.7rem; color: var(--bb-ink-soft); }
  /* A 44px button with no border and no background is a keyboard dead spot
     without this — the same ring `button.sv-cell` carries. */
  .compliance-door:focus-visible {
    outline: 2px solid var(--bb-vermillion);
    outline-offset: 2px;
  }
  .charter-compliance[data-open] .compliance-door-caret {
    display: inline-block;
    transform: rotate(180deg);
  }
  .charter-compliance .compliance-head { display: none; }
  .charter-compliance:not([data-open]) .compliance-list,
  .charter-compliance:not([data-open]) .compliance-hint { display: none; }

  /* COUPLED RULE, REVERSED (#218). This tier used to HIDE the plan's own tally
   * whenever the survey had an account, because the survey said the same things
   * in more detail directly beneath it. The survey no longer does, so the tally
   * is the only account of the plan and always letters.
   *
   * The old rule is DELETED rather than overridden, deliberately. It was
   * `.charter-recto:not(:has(~ .charter-colophon .sv-lead)) .charter-plate
   * figcaption #charter-plate-cap`, which is (1,5,1): `:has()` takes the
   * specificity of its most specific argument and `:not()` takes its
   * argument's. A natural-looking override is (1,3,1) and loses outright —
   * source order never gets to decide, the tally simply stays hidden, and the
   * page looks fine.
   *
   * The legend takes `.plate-founded`'s register — a short gilt rule, the label
   * in small caps above it — because that is the established form for a second
   * line inside this caption, rather than a phone-only caption style. */
  .charter-plate figcaption { position: relative; padding-top: 2px; }
  .charter-plate figcaption #charter-plate-cap {
    display: block;
    position: relative;
    margin-top: 7px;
    padding-top: 8px;
  }
  .charter-plate figcaption #charter-plate-cap::before {
    content: "";
    position: absolute;
    top: 0;
    left: 50%;
    width: 46px;
    margin-left: -23px;
    border-top: 1px solid var(--bb-gilt);
  }
  .charter-plate figcaption b { font-size: 0.72rem; letter-spacing: 0.16em; }
  .charter-plate figcaption span { font-size: 1.02rem; }

  /* THE SURVEY, CUT TO TWO ROWS. The header block carries the editorial rule;
   * this is its mechanism. The tier enumerates what it DROPS, by name, rather
   * than what it keeps — so a figure added to the painter tomorrow appears on a
   * phone by default, where the reader can object to it, instead of vanishing
   * with nothing to say so. `data-fig` is written by surveyCell. */
  .sv-row > .sv-cell[data-fig="chambers"],
  .sv-row > .sv-cell[data-fig="garrison"],
  .sv-row > .sv-cell[data-fig="household"],
  .sv-row > .sv-cell[data-fig="special"],
  .sv-row > .sv-cell[data-fig="walls"],
  .sv-row > .sv-cell[data-fig="treasury"],
  .sv-row > .sv-cell[data-fig="turn"] { display: none; }
  /* "Start tracking" goes with the Turn cell it stood beside. The Chronicle
   * keeps #btn-begin-turns, which was the feature's ONLY entry point before
   * #304 put this arm on the Charter, so no route is lost and the older
   * anti-nag reading is restored on the tier that can least afford the line. */
  .sv-row > .sv-verb { display: none; }
  /* The two survivors read as account-book rows: label hard left, a dotted
   * leader, the figure at the right margin. `line-height` drops to 1.35 — the
   * 2.2 this block used to carry was written for WRAPPED LINES of running
   * inline text (the desktop grammar), and once the same block made every cell
   * a block box it stopped spacing anything and became ~22px of padding per
   * figure, nine figures deep. Nobody chose that. */
  .sv-row { line-height: 1.35; }
  .sv-row > .sv-cell {
    display: grid;
    grid-template-columns: auto minmax(14px, 1fr) auto auto;
    align-items: baseline;
    width: 100%;
    margin-right: 0;
    padding: 3px 0;
    text-align: left;
    /* Not cosmetic: `.sv-cell` is `nowrap` to hold a figure together on a
       flowing line, and a full-measure cell that cannot wrap overflows. */
    white-space: normal;
  }
  .sv-row > .sv-cell::before {
    content: "";
    grid-column: 2;
    align-self: center;
    margin: 0 6px;
    border-bottom: 1px dotted var(--bb-rule-soft);
  }
  .sv-row > .sv-cell > .sv-l { grid-column: 1; margin-right: 0; }
  .sv-row > .sv-cell > .sv-v { grid-column: 3; justify-self: end; }
  .sv-row > .sv-cell > .sv-u { grid-column: 4; min-width: 0; margin-left: 5px; }
  /* The interpunct has no work left — a ledger row is closed by its own leader.
   * BOTH base selectors are restated under `.sv-row >` rather than overridden by
   * a bare `.sv-cell::after`: `:has()` takes the specificity of its most
   * specific argument, so the base rules are (0,3,0) and a bare override is
   * (0,2,0) — it loses outright and source order never gets to decide. That
   * failure is silent; the mark simply stays. */
  .sv-row > .sv-cell:has(~ .sv-cell:not([hidden]))::after,
  .sv-row > .sv-cell:has(~ .sv-follow:not([hidden]))::after { content: none; }
  /* The circulation line is a SENTENCE about this document, not a figure about
   * the fabric, so it keeps the stacked-line grammar and never becomes a ledger
   * row — it carries `.sv-row` too, so every rule above reaches it and every one
   * below takes it back at equal specificity, later in source order. `.circ`,
   * the relay lending slip, is not an `.sv-row` at all and none of this touches
   * it. */
  .sv-circulation { line-height: 1.45; }
  .sv-circulation > .sv-cell { display: block; padding: 0; }
  .sv-circulation > .sv-cell::before { content: none; }
  .sv-circulation > .sv-cell > .sv-l { margin-right: 6px; }
  .sv-lead { text-align: center; }
  /* The slip's roll never runs more than two columns at this measure (#304) —
   * the inline column-count is a maximum for the desktop band, and four columns
   * of names in 366px is unreadable. The facts stack rather than sitting on one
   * baseline, and the verbs lose their right-pole so they read in order. */
  .circ-slots { column-count: 2 !important; column-gap: 14px; }
  .circ-facts { gap: 4px 18px; }
  .circ-verbs { margin-left: 0; }
}

/* ---- The Grounds (chapter II, §5.9) ------------------------------------- */
/* Full-canvas floorplan editor — a flex column: head (fixed), the temporary
 * facilities panel + compliance banner, then the scrolling .stage. The rail
 * is gone. */
.chapter-grounds {
  display: flex;
  flex-direction: column;
  min-height: 0;
}
/* The Grounds chapter-head band is RETIRED (#230 pt1): both its ▾ menus moved
 * out (Export → app top bar, Map display → canvas view-dock), so the Grounds
 * chapter leads straight into .grounds-body with no header band. The generic
 * .chapter-head still serves the Register. */

/* Compact, read-only cap echo (§5.6). */
.cap-echo {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 6px 10px;
  background: var(--bb-parchment);
  border: 1px solid var(--bb-rule-soft);
  cursor: default;
}
.cap-echo .cap-echo-label {
  font-family: var(--bb-font-display);
  font-size: 0.68rem;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--bb-ink-faint);
}
.cap-echo .cap-echo-count {
  font-family: var(--bb-font-display);
  font-variant-numeric: lining-nums tabular-nums;
  font-size: 1rem;
  /* Tight line-box so the display-font digits sit optically centered against the
   * label + meter — the inherited 1.45 line-height floated the count high. */
  line-height: 1;
  /* Keep "12 / 8" on one line — a two-digit count was wrapping at the slash
   * when the aside squeezed the flex row (the meter shrinks first instead). */
  white-space: nowrap;
  color: var(--bb-ink);
}
.cap-echo .cap-echo-level {
  font-family: var(--bb-font-italic);
  font-style: italic;
  font-size: 0.78rem;
  color: var(--bb-ink-faint);
}
.cap-echo .cap-echo-meter {
  width: 72px;
  height: 6px;
  background: var(--bb-parchment-mid);
  border: 1px solid var(--bb-rule-soft);
  overflow: hidden;
}
.cap-echo .cap-echo-fill {
  height: 100%;
  width: 0;
  background: var(--bb-gilt);
}
.cap-echo .cap-echo-fill.over { background: var(--bb-danger); }

/* ⊕ Commission a facility — the add-flow entry point (Grounds head + Register
 * header). Elevated to resting gilt-fill outline per #157 steward verdict:
 * handout owns the lone gilt primary, so Commission uses the outline family
 * with gilt-fill background at rest (NOT solid gilt primary). */
.commission-btn {
  flex: 0 0 auto;
  align-self: center;
  background: var(--bb-gilt-fill);
  border: 1px solid var(--bb-gilt);
  border-radius: 0;
  box-shadow: none;
  color: var(--bb-gilt-deep);
  padding: 9px 16px;
  font-family: var(--bb-font-display);
  font-size: 0.82rem;
  letter-spacing: 0.04em;
  cursor: pointer;
  white-space: nowrap;
}
.commission-btn:hover {
  border-color: var(--bb-gilt-deep);
}
.commission-btn:focus-visible {
  outline: 2px solid var(--bb-vermillion);
  outline-offset: 2px;
}

/* ---- The placeable-content library — right column (#161, §5.9) ---------- */
/* The canvas (hero) and the library aside sit in a flex row; each scrolls on its
 * own, so neither moves the other (§5.9, "the column scrolls independently; the
 * canvas stays put"). Two zones only in the interim — the left plate-spine,
 * single-active-floor view, and caliper seam are #160 (deferred); the canvas
 * keeps its stacked-floor scroll and the seam here is a plain --bb-rule hairline,
 * NOT the caliper. */
/* The working frame (#275): the command rail stacked over the three-zone body.
 * It exists for two reasons — it gives the rail a full-chapter-width slot above
 * the body (see .grounds-command), and it is the fullscreen target, so the tools
 * AND the library travel into fullscreen with the canvas. */
.grounds-frame {
  flex: 1 1 auto;
  min-height: 0;
  display: flex;
  flex-direction: column;
}
.grounds-body {
  flex: 1 1 auto;
  min-height: 0;
  display: flex;
  align-items: stretch;
}
/* The scale-ruler divider between the canvas and the library (#160). It IS the
 * column divider AND an illustrative 5-ft ruler: ticks per grid row, majors
 * every five squares labelled in running feet. The <svg.seam-ruler> inside is
 * painted + kept grid-true by floorplan-view.js drawSeamRuler() — the seam sits
 * outside the zoom/scroll container, so its ticks are synced by JS to track zoom
 * (pitch) and canvas scroll (offset). Dissolves into the aside's dashed shelf-top
 * border when the library folds below the canvas on the narrow layout. */
.grounds-seam {
  flex: 0 0 34px;
  align-self: stretch;
  position: relative;
  overflow: hidden;
  background-color: var(--bb-parchment-mid);
  border-left: 1px solid var(--bb-rule-soft);
  border-right: 1px solid var(--bb-rule-soft);
}
.seam-ruler {
  position: absolute;
  inset: 0;
  display: block;
  pointer-events: none;
}
.seam-ruler .seam-tick { stroke: var(--bb-rule-soft); stroke-width: 1; }
.seam-ruler .seam-tick.major { stroke: var(--bb-ink-soft); stroke-width: 1.1; }
.seam-ruler .seam-label {
  fill: var(--bb-ink-faint);
  font-family: var(--bb-font-display);
  font-size: 8.5px;
  letter-spacing: 0.02em;
  font-variant-numeric: lining-nums tabular-nums;
}
/* The library: narrow, quiet (one parchment step down from the canvas page), its
 * own scroll. No accent fill / shadow — the canvas stays unmistakably the hero. */
.grounds-aside {
  flex: 0 0 288px;
  min-height: 0;
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  background: var(--bb-parchment);
  /* No padding-bottom: the sticky milestone footer (.grounds-since) sits flush
   * to the aside's bottom edge and carries its own padding; a container
   * padding-bottom would leave a parchment gap below it when scrolled to end.
   * The caliper seam (#160) is the wide-layout divider; the ≤700px shelf
   * restores a dashed top border in the mobile block. */
}
/* Commission sits at the top of the column and stays put while chips scroll under
 * it — its own parchment backing so nothing bleeds through the gilt-fill. */
.grounds-aside-head {
  position: sticky;
  top: 0;
  z-index: 2;
  display: flex;
  flex-direction: column;
  gap: 10px;
  padding: 14px 14px 10px;
  background: var(--bb-parchment);
  border-bottom: 1px solid var(--bb-rule-soft);
}
.grounds-aside .commission-btn {
  display: block;
  width: 100%;
  text-align: center;
}
/* The cap-echo fills the aside-head width, beneath Commission. */
.grounds-aside-head .cap-echo { justify-content: center; }
/* A category block: a small-caps rule label (reusing the tray/cap-echo label
 * idiom) over a hint line and the chips. Generic so #143's Terrain drops in as a
 * second .library-cat with no new scaffolding. */
.library-cat {
  display: flex;
  flex-direction: column;
  padding: 12px 14px 0;
}
.library-cat-label {
  margin: 0;
  padding-bottom: 6px;
  border-bottom: 1px solid var(--bb-rule-soft);
  font-family: var(--bb-font-display);
  font-size: 0.7rem;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--bb-ink-faint);
}
.library-cat .tray-note {
  margin: 8px 0 10px;
  font-family: var(--bb-font-italic);
  font-style: italic;
  font-size: 0.8rem;
  color: var(--bb-ink-faint);
}
.library-cat .tray-chips {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  min-width: 0;
}
.library-empty {
  margin: 2px 0 0;
  font-family: var(--bb-font-italic);
  font-style: italic;
  font-size: 0.8rem;
  color: var(--bb-ink-faint);
}

/* Furnish mode palette (#232): occupies the library's column, shown only while
 * the Furnish tool is armed. CSS-gated on the edit mode so no JS toggling — the
 * facilities library and the prop palette are mutually exclusive right docks. */
.furnish-palette { display: none; }
body[data-edit-mode="furnishings"] .grounds-aside { display: none; }
body[data-edit-mode="furnishings"] .furnish-palette {
  /* --furnish-head-h is the sticky-head height AND the offset the sticky
     category labels pin at, so the two sticky layers share one number and
     never overlap. Tuned to the compact title+search head; nudge if the
     head font-size changes. */
  --furnish-head-h: 74px;
  flex: 0 0 300px;
  min-height: 0;
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  background: var(--bb-parchment);
}
.furnish-palette-head {
  position: sticky;
  top: 0;
  z-index: 1;
  padding: 12px 14px 10px;
  background: var(--bb-parchment);
  border-bottom: 1px solid var(--bb-rule-soft);
}
.furnish-palette-title {
  margin: 0 0 8px;
  font-family: var(--bb-font-display);
  font-size: 0.7rem;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--bb-ink-faint);
}
.furnish-search {
  width: 100%;
  box-sizing: border-box;
  padding: 6px 9px;
  /* --bb-font-text: --bb-font-body was never defined, so this field silently
     rendered in the UA font while every other input uses the text face (#218). */
  font-family: var(--bb-font-text);
  font-size: 0.85rem;
  color: var(--bb-ink);
  background: var(--bb-parchment-light);
  border: 1px solid var(--bb-rule-soft);
}
.furnish-search:focus { outline: none; border-color: var(--bb-gilt); }
/* Themed clear ✕ (#218) — same recipe as .help-search-clear. */
.furnish-search-wrap { position: relative; }
.furnish-search { padding-right: 2rem; /* room for the ✕ */ }
.furnish-search::placeholder {
  color: var(--bb-ink-faint);
  font-family: var(--bb-font-italic);
  font-style: italic;
}
.furnish-search-clear {
  position: absolute;
  right: 8px;
  top: 50%;
  transform: translateY(-50%);
  background: none;
  border: none;
  padding: 0 2px;
  color: var(--bb-ink-faint);
  cursor: pointer;
  font-size: 1rem;
  line-height: 1;
}
.furnish-search-clear:hover { color: var(--bb-vermillion); }
/* Keep centering under the global button:active nudge (see .help-search-clear). */
.furnish-search-clear:active { transform: translateY(calc(-50% + 1px)); }
.furnish-search-clear:focus-visible { outline: 2px solid var(--bb-vermillion); outline-offset: 1px; }
.furnish-note {
  margin: 0;
  padding: 10px 14px 0;
  font-family: var(--bb-font-italic);
  font-style: italic;
  font-size: 0.78rem;
  color: var(--bb-ink-faint);
}
.furnish-palette-body { padding: 10px 14px 16px; }
.furnish-group { margin-top: 12px; }
.furnish-group:first-child { margin-top: 2px; }
.furnish-group-label {
  /* Sticky category running-head: pins just under the palette head (z 1 > this
     z 0, so labels slide under it) on an opaque bar so chips don't ghost
     through. Full-bleed via negative margins against the body's 14px padding. */
  position: sticky;
  top: var(--furnish-head-h);
  z-index: 0;
  margin: 0 -14px 8px;
  padding: 4px 14px 5px;
  background: var(--bb-parchment);
  border-bottom: 1px solid var(--bb-rule-soft);
  font-family: var(--bb-font-display);
  font-size: 0.66rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--bb-ink-faint);
}
.furnish-chips {
  display: grid;
  /* 3-up at the 300px dock (272px content → 85.3px chips, 75.3px of caption
     box). 4-up cannot hold this register: the longest single word in the
     74-prop catalog needs ~69px against the 54px a 62px chip left, so ALL FOUR
     themes overflowed — Cartographer was just the only one whose excess
     (+26.6px on "Candelabrum") beat the 8px gap and landed on the neighbour
     instead of in the gutter. Cinzel sets the width budget: it is ~15% wider
     than any other theme's display face. Re-measure against it before adding a
     prop whose longest word exceeds 12 characters. */
  grid-template-columns: repeat(auto-fill, minmax(78px, 1fr));
  gap: 8px;
}
.furnish-chip {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 5px;
  padding: 8px 4px 6px;
  background: var(--bb-parchment-light);
  border: 1px solid var(--bb-rule-soft);
  cursor: pointer;
  user-select: none;
}
.furnish-chip:hover { border-color: var(--bb-gilt); }
/* #218 — the palette had no focus ring at all and fell back to the UA outline.
   Mirrors .kind-tile:focus-visible, and vermillion is already this chip's
   armed colour. */
.furnish-chip:focus-visible { outline: 2px solid var(--bb-vermillion); outline-offset: -2px; }
.furnish-chip.armed { border-color: var(--bb-vermillion); background: var(--bb-parchment-mid); }
.furnish-chip-art {
  width: 32px;
  height: 32px;
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--bb-ink-soft);
}
.furnish-chip-art .glyph-svg { max-width: 100%; max-height: 100%; display: block; }
.furnish-chip-name {
  /* A prop name is a NAME: core spec §2.6 puts names on --bb-font-text and
     reserves --bb-font-display for the wordmark, headings and button/section
     labels. The palette's own chrome (.furnish-palette-title,
     .furnish-group-label) keeps the display face, so the panel still speaks it
     where it belongs. */
  font-family: var(--bb-font-text);
  font-size: 0.78rem;
  letter-spacing: 0;
  line-height: 1.2;
  /* Bound the caption to the chip's content box. As a flex item in a column
     chip it otherwise floors at min-content — its widest WORD — and
     align-items:center splits the excess out both sides. */
  align-self: stretch;
  min-width: 0;
  text-align: center;
  color: var(--bb-ink-soft);
}
/* R6.3: shown by furnish-mode.js when a search hides every group — otherwise
   the body just goes blank and reads as broken. */
.furnish-empty {
  margin: 0;
  padding: 4px 14px 16px;
  font-family: var(--bb-font-italic);
  font-style: italic;
  font-size: 0.82rem;
  color: var(--bb-ink-faint);
}
.furnish-clear {
  padding: 0;
  border: none;
  background: none;
  font: inherit;
  font-style: normal;
  color: var(--bb-gilt);
  text-decoration: underline;
  cursor: pointer;
}
/* Selected-prop outline in Furnish mode (#232). */
.furnish-sel {
  fill: none;
  stroke: var(--bb-vermillion);
  stroke-width: 1.5;
  stroke-dasharray: 4 3;
}

.tray-chip {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 5px 6px 5px 9px;
  background: var(--bb-parchment-light);
  border: 1px solid var(--bb-rule-soft);
  cursor: grab;
  touch-action: pan-y;     /* page scroll on touch; horizontal drag → canvas */
  user-select: none;
}
.tray-chip:hover { border-color: var(--bb-gilt); }
.tray-chip.placing { border-color: var(--bb-vermillion); background: var(--bb-parchment-mid); }
.tray-chip .tray-chip-glyph {
  width: 18px;
  height: 18px;
  flex: 0 0 auto;
  display: inline-flex;
}
.tray-chip.special .tray-chip-glyph { color: var(--bb-vermillion); }
.tray-chip.basic .tray-chip-glyph { color: var(--bb-sapphire, var(--bb-ink-soft)); }
.tray-chip .tray-chip-name {
  font-family: var(--bb-font-display);
  font-size: 0.86rem;
  letter-spacing: 0.02em;
  color: var(--bb-ink);
}
.tray-chip .tray-chip-size {
  font-family: var(--bb-font-italic);
  font-style: italic;
  font-size: 0.78rem;
  color: var(--bb-ink-faint);
  font-variant-numeric: lining-nums tabular-nums;
}
.tray-chip .tray-chip-remove {
  flex: 0 0 auto;
  width: 20px;
  height: 20px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  background: transparent;
  border: none;
  box-shadow: none;
  border-radius: 0;
  color: var(--bb-ink-faint);
  font-size: 1.05rem;
  line-height: 1;
  cursor: pointer;
}
.tray-chip .tray-chip-remove:hover { color: var(--bb-parchment-light); background: var(--bb-danger); }

/* Off-map shelf chip (#195) — neutral, never amber (that's construction) and
   never greyed (greyed reads "disabled"; this is a deliberate, resolved
   location). Sapphire is already the app's cool/neutral accent (basic-room
   glyphs), so it reads as informational rather than a warning. */
.tray-chip.offmap { border-color: var(--bb-sapphire); }
.tray-chip.offmap:hover { border-color: var(--bb-sapphire-deep); }
/* Name + location note stack in a column so the note owns its own row (#218). */
.tray-chip .tray-chip-text {
  display: flex;
  flex-direction: column;
  min-width: 0;
  gap: 1px;
}
.tray-chip .tray-chip-text .tray-chip-name { overflow-wrap: anywhere; }
.tray-chip .tray-chip-note {
  font-family: var(--bb-font-italic);
  font-style: italic;
  font-size: 0.78rem;
  color: var(--bb-ink-faint);
  /* Stacked beneath the name: wrap up to two lines instead of ellipsizing a
     short note like "in town" down to a few letters. Full text lives in the
     title for the rare long note that still overflows. */
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  overflow-wrap: anywhere;
}
.library-cat-offmap .library-cat-label {
  color: var(--bb-sapphire-deep);
  border-bottom-color: var(--bb-sapphire);
}

/* ---- Commission dialog (#121, §5.9) ------------------------------------- */
/* Hosts the kind → special / basic pickers inside a modal. The pickers carry
 * their own list styling; give them room and drop the rail margins. */
#commission-dialog .picker {
  margin: 8px 0 0;
  padding: 0;
  background: transparent;
  border: none;
  box-shadow: none;
}
#commission-dialog .picker-list {
  max-height: 56vh;
  overflow-y: auto;
  scrollbar-gutter: stable; /* reserve the scrollbar's own lane — don't overlap the cards */
  padding-right: 4px;
}
/* The sheet is NOT a .picker-list, so the 56vh cap above no longer contains the
 * basic-room flow — the dialog itself has to be the scroller or a phone runs it
 * off the bottom. Mirrors #vtt-export-dialog: cap the form, let the body scroll,
 * and pin the actions row so Cancel never scrolls away. */
#commission-dialog .dialog-form {
  max-height: min(86vh, 900px);
  display: flex;
  flex-direction: column;
  min-height: 0;
}
#commission-dialog #commission-sheet {
  overflow-y: auto;
  scrollbar-gutter: stable;
  padding-right: 12px;
  min-height: 0;
}
#commission-dialog .dialog-actions { flex-shrink: 0; }

/* ---- The commissioning sheet (#218) ------------------------------------- */
/* The drafting table: choices left, docket right. Shared field furniture first
 * (.cm-*), then the docket itself (.cw-*). */

.cm-field { min-width: 0; }
/* An eyebrow with a rule running out to the right — the section marker used
 * down the whole left column, so Kind / Type / Name / Extent / How many read as
 * one ruled sheet rather than four stacked controls. */
.cm-eyebrow {
  font-family: var(--bb-font-display);
  font-size: 0.6875rem;             /* 11px */
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--bb-ink-soft);
  display: flex;
  align-items: baseline;
  gap: 8px;
  margin-bottom: 6px;
}
.cm-eyebrow::after {
  content: "";
  flex: 1 1 auto;
  height: 1px;
  background: var(--bb-rule-soft);
}
.cm-name-input {
  width: 100%;
  box-sizing: border-box;
  font-family: var(--bb-font-display);
  font-size: 1.0625rem;             /* 17px */
  color: var(--bb-ink);
  background: var(--bb-parchment-light);
  border: 1px solid var(--bb-rule);
  /* The gilt underline marks the one free-text field on the sheet — it is the
   * only thing here the user writes rather than picks. */
  border-bottom-width: 2px;
  border-bottom-color: var(--bb-gilt-deep);
  border-radius: 0;
  padding: 8px 10px;
}
.cm-name-input:focus { outline: none; border-color: var(--bb-vermillion); }

/* The picked mark. A SHAPE channel beside the colour one — a picked tile must
 * never be identifiable by hue alone. */
.cm-tick {
  flex: 0 0 auto;
  width: 15px; height: 15px;
  border-radius: 50%;
  border: 1px solid var(--bb-rule);
  background: var(--bb-parchment-light);
  display: grid;
  place-items: center;
  align-self: center;
}
.cm-tick::after {
  content: "";
  width: 7px; height: 7px;
  border-radius: 50%;
  background: transparent;
}
.is-picked .cm-tick { border-color: var(--bb-vermillion); }
.is-picked .cm-tick::after { background: var(--bb-vermillion); }

/* Kind / type tiles. Real radios: the <input> is visually hidden and the
 * <label> is the tile, so arrow keys roam the group for free. */
.cm-type-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 6px; }
.cm-type-grid--wide { grid-template-columns: 1fr; }
.cm-type {
  display: flex;
  align-items: center;
  gap: 7px;
  padding: 7px 8px;
  min-height: 44px;                 /* touch target */
  border: 1px solid var(--bb-rule);
  background: var(--bb-parchment-light);
  color: var(--bb-ink);
  cursor: pointer;
  text-align: left;
  font-family: var(--bb-font-display);
  font-size: 0.8125rem;             /* 13px */
  line-height: 1.15;
}
.cm-type .glyph { width: 24px; height: 24px; }
.cm-type .glyph .glyph-svg { width: 15px; height: 15px; }
.cm-type:hover { border-color: var(--bb-sapphire); }
/* Visually hidden, not display:none — a display:none radio leaves the group
 * unreachable by keyboard. */
.cm-type input, .cw-chip input {
  position: absolute;
  opacity: 0;
  width: 0;
  height: 0;
  margin: 0;
  pointer-events: none;
}
.cm-type.is-picked {
  border-color: var(--bb-sapphire-deep);
  box-shadow: inset 0 0 0 1px var(--bb-sapphire-deep);
  background: var(--bb-basic-fill);
}
/* Keyboard focus has to be visible on the LABEL, since the input it belongs to
 * is the hidden half of the pair. */
.cm-type:focus-within { outline: 2px solid var(--bb-vermillion); outline-offset: 1px; }
.cm-type.is-custom {
  grid-column: span 2;
  font-style: italic;
  font-family: var(--bb-font-italic);
  font-size: 0.875rem;              /* 14px */
}
.cm-type-grid--wide .cm-type.is-custom { grid-column: auto; }
.cm-type-name { min-width: 0; overflow-wrap: anywhere; }

/* The special's catalog line, shown on its sheet. */
.cw-blurb {
  margin: 0;
  font-family: var(--bb-font-text);
  font-size: 0.875rem;
  line-height: 1.45;
  color: var(--bb-ink-soft);
  border-left: 2px solid var(--bb-rule);
  padding-left: 10px;
}

/* Extent chips — the size tiers, priced. */
.cw-chip-row { display: flex; gap: 6px; }
.cw-chip {
  flex: 1 1 0;
  min-width: 0;
  padding: 7px 9px;
  min-height: 44px;
  border: 1px solid var(--bb-rule);
  background: var(--bb-parchment-light);
  font-family: var(--bb-font-display);
  font-size: 0.9375rem;             /* 15px */
  color: var(--bb-ink);
  cursor: pointer;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  justify-content: center;
}
.cw-chip small {
  font-family: var(--bb-font-italic);
  font-style: italic;
  font-size: 0.6875rem;             /* 11px */
  color: var(--bb-ink-soft);
  font-variant-numeric: lining-nums tabular-nums;
  white-space: nowrap;
}
.cw-chip.is-picked {
  border-color: var(--bb-vermillion);
  background: rgba(168, 40, 29, 0.06);
  box-shadow: inset 0 0 0 1px var(--bb-vermillion);
}
.cw-chip:focus-within { outline: 2px solid var(--bb-vermillion); outline-offset: 1px; }
/* A special's tier is dictated by its catalog entry, so it reads as a fact
 * rather than offering a choice the rules don't have. */
.cw-extent-fixed {
  margin: 0;
  font-family: var(--bb-font-text);
  font-size: 0.875rem;              /* 14px */
  color: var(--bb-ink);
  font-variant-numeric: lining-nums tabular-nums;
}

/* How many (basic rooms only) — "a wing with six bedrooms" is one commission. */
.cw-qty { display: flex; align-items: stretch; gap: 0; width: max-content; }
.cw-qty-btn {
  width: 38px;
  min-height: 38px;
  padding: 0;
  border: 1px solid var(--bb-rule);
  background: var(--bb-parchment-light);
  color: var(--bb-ink);
  font-family: var(--bb-font-display);
  font-size: 1.125rem;
  line-height: 1;
  cursor: pointer;
}
.cw-qty-btn:hover:not([disabled]) { border-color: var(--bb-vermillion); color: var(--bb-vermillion); }
.cw-qty-btn[disabled] { opacity: 0.4; cursor: not-allowed; }
.cw-qty-input {
  /* A global 120px min-width floor applies to inputs — beat it explicitly or
   * the stepper is as wide as the column. */
  width: 56px;
  min-width: 0;
  text-align: center;
  border: 1px solid var(--bb-rule);
  border-left: none;
  border-right: none;
  border-radius: 0;
  background: var(--bb-parchment-light);
  font-family: var(--bb-font-display);
  font-size: 1rem;
  color: var(--bb-ink);
  font-variant-numeric: lining-nums tabular-nums;
}

/* ---- The docket --------------------------------------------------------- */
.cw-grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 306px;
  gap: 22px;
  align-items: start;
}
.cw-choices { display: flex; flex-direction: column; gap: 14px; min-width: 0; }
/* Copy mode (#307) has three short fields where a commission has five controls
   and a type grid, so the elastic left column would open a hole beside a full
   docket. Cap it and centre the pair: the sheet narrows to what it is actually
   carrying instead of holding the commission flow's width with nothing in it. */
.picker.is-copy .cw-grid {
  grid-template-columns: minmax(0, 250px) 306px;
  justify-content: center;
}
.cw-docket {
  border: 1px solid var(--bb-rule);
  border-top: 3px solid var(--bb-gilt-deep);
  background: var(--bb-parchment-light);
  box-shadow: var(--shadow-card);
  padding: 12px 14px 14px;
  display: flex;
  flex-direction: column;
  gap: 9px;
}
.cw-docket-eyebrow {
  font-family: var(--bb-font-display);
  font-size: 0.625rem;              /* 10px */
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--bb-ink-faint);
  margin: 0;
}
.cw-docket-name {
  margin: -3px 0 0;
  font-family: var(--bb-font-display);
  font-size: 1.25rem;               /* 20px */
  color: var(--bb-ink);
  line-height: 1.15;
  overflow-wrap: anywhere;
}
/* The waiting state is real content a reader must be able to read, so it takes
 * --bb-ink-soft; --bb-ink-faint is for decoration. */
.cw-docket-name.is-empty {
  font-family: var(--bb-font-italic);
  font-style: italic;
  font-size: 1rem;
  color: var(--bb-ink-soft);
}
.cw-plan { display: flex; justify-content: center; }
.cw-plan svg { touch-action: none; }        /* the corner handle owns the drag */
.cw-plan-cap {
  text-align: center;
  font-family: var(--bb-font-italic);
  font-style: italic;
  font-size: 0.75rem;               /* 12px */
  color: var(--bb-ink-soft);
  margin: -3px 0 0;
}
.cw-plan-grab { cursor: nwse-resize; }
/* Copy mode (#307): the plan slot holds the chamber as it stands instead of
   the graph-paper control. Matted and ruled so it reads as a drawn plate
   rather than as the interactive plan it replaces — there is nothing to drag
   here, and the cursor must not promise otherwise. */
.cw-plan.is-plate {
  padding: 6px;
  background: var(--bb-parchment-light);
  border: 1px solid var(--bb-rule);
  border-radius: 2px;
}
.cw-plan.is-plate svg { touch-action: auto; cursor: default; }
.cw-carries,
.cw-leaves {
  font-size: 0.75rem;               /* 12px */
  line-height: 1.45;
  margin: 2px 0 0;
  color: var(--bb-ink-soft);
}
/* The one thing that stays behind reads as an aside, not as a second fact of
   equal weight — it appears only when the source actually has one. */
.cw-leaves {
  font-family: var(--bb-font-italic);
  font-style: italic;
  color: var(--bb-ink-faint);
}
.cw-dims {
  display: flex;
  align-items: center;
  gap: 6px;
  flex-wrap: wrap;
}
.cw-dim { display: flex; align-items: center; gap: 5px; }
.cw-dim span {
  font-family: var(--bb-font-display);
  font-size: 0.6875rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--bb-ink-soft);
}
.cw-dim input {
  width: 52px;
  min-width: 0;                     /* beats the global 120px input floor */
  padding: 4px 6px;
  text-align: center;
  border: 1px solid var(--bb-rule);
  border-radius: 0;
  background: var(--bb-parchment-light);
  font-family: var(--bb-font-display);
  font-size: 0.9375rem;
  color: var(--bb-ink);
  font-variant-numeric: lining-nums tabular-nums;
}
.cw-dim-x { color: var(--bb-ink-faint); }
.cw-dim-note {
  flex: 1 1 100%;
  font-family: var(--bb-font-italic);
  font-style: italic;
  font-size: 0.6875rem;
  color: var(--bb-ink-soft);
}
.cw-dim-note.is-over { color: var(--bb-vermillion-deep); font-style: normal; }
.cw-tally {
  margin: 0;
  display: grid;
  grid-template-columns: auto 1fr;
  gap: 3px 10px;
  border-top: 1px solid var(--bb-rule-soft);
  padding-top: 9px;
}
.cw-tally dt {
  font-family: var(--bb-font-display);
  font-size: 0.6875rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--bb-ink-soft);
  align-self: baseline;
}
.cw-tally dd {
  margin: 0;
  font-family: var(--bb-font-text);
  font-size: 0.9375rem;
  color: var(--bb-ink);
  font-variant-numeric: lining-nums tabular-nums;
  text-align: right;
}
.cw-dest {
  margin: 0;
  font-family: var(--bb-font-italic);
  font-style: italic;
  font-size: 0.75rem;
  color: var(--bb-ink-soft);
  line-height: 1.35;
  border-top: 1px solid var(--bb-rule-soft);
  padding-top: 8px;
}
/* Armed from the blank-grid "Commission here" (#196): the room lands where you
 * clicked, not in the tray. Said before you commit, in the colour the app uses
 * for a consequence. */
.cw-dest.is-armed { color: var(--bb-vermillion-deep); }
/* The one control on the sheet that creates the facility. */
.cw-seal {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 9px;
  width: 100%;
  min-height: 44px;
  padding: 9px 12px;
  cursor: pointer;
  background: linear-gradient(180deg, var(--bb-gilt-bright), var(--bb-gilt));
  color: var(--bb-ink);
  border: 1px solid var(--bb-gilt-deep);
  border-radius: 0;
  box-shadow: 0 1px 0 var(--bb-inset-strong) inset, 0 2px 4px var(--bb-shadow);
  font-family: var(--bb-font-display);
  font-size: 0.8125rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  white-space: nowrap;
}
.cw-seal .glyph-svg { width: 18px; height: 18px; fill: currentColor; flex: 0 0 auto; }
.cw-seal[disabled] { opacity: 0.42; cursor: not-allowed; box-shadow: none; }

/* WIDTH-ONLY, deliberately not the compound phone tier: that tier's height
 * branch fires on a landscape phone, and collapsing two columns into one there
 * makes the dialog TALLER on the one viewport with no height to spare. The
 * docket follows the choices in source order, so stacking already puts the
 * gilt Commission last — under the thumb. */
@media (max-width: 760px) {
  /* Copy mode's fixed pair is named explicitly: it is a compound selector, so
     the bare .cw-grid reset below cannot reach it on specificity alone and the
     choices column would be squeezed off the side of a phone. Same specificity,
     later in source order — which is the only thing deciding it. */
  .cw-grid,
  .picker.is-copy .cw-grid { grid-template-columns: 1fr; gap: 14px; }
}

/* The "added since milestone" strip is the facilities aside's FOOTER (#230,
 * §5.9): a sticky-bottom band mirroring the sticky-top .grounds-aside-head, so
 * the column reads as head / scrolling chips / foot — the milestone chrome
 * brackets the content instead of floating loose inside it. It sheds the
 * .defenses-line recessed-well box (that four-sided border is what read
 * "heavy/orphaned") for flush parchment with a single top hairline; the
 * .defenses-label/.defenses-stats typography inside is kept. `margin-top:auto`
 * pins it to the bottom edge when content is short; `sticky` keeps Mark
 * milestone / Show new in view while chips scroll. Scoped to beat .defenses-line
 * on specificity. Hidden until render, so the rule only shows with content. */
.grounds-aside .grounds-since {
  position: sticky;
  bottom: 0;
  z-index: 2;
  margin-top: auto;
  background: var(--bb-parchment);
  border: 0;
  border-top: 1px solid var(--bb-rule-soft);
  border-radius: 0;
  padding: 10px 14px 14px;
}
/* The footer is always ~288px wide on desktop (not just on the ≤700px phone
 * shelf), so it needs the two-row stacking unconditionally (#230 footer
 * follow-up): label+name / delta full-width / the two pills + help ⓘ on one row.
 * The delta takes its own row; the pills then pack together and the ⓘ right-
 * aligns (margin-left:auto) onto that same row so it never orphans to a line of
 * its own — pills stay left-grouped, ⓘ trails at the right edge. */
.grounds-aside .grounds-since { gap: 6px; }
.grounds-aside .grounds-since .defenses-stats { flex-basis: 100%; }
/* Shrink the pills so "Show new" + "Mark milestone" actually fit one row in the
 * ~260px aside — drop the 8.5em min-width, tighten tracking/padding, and let the
 * flex gap (6px) do the spacing (not the base 8px margin). The help ⓘ then
 * right-aligns onto that same row so it never orphans to a line of its own. */
.grounds-aside .grounds-since .milestone-highlight-btn { min-width: 0; }
.grounds-aside .grounds-since .milestone-highlight-btn,
.grounds-aside .grounds-since .milestone-mark-btn {
  margin-left: 0;
  letter-spacing: 0.05em;
  padding: 3px 6px;
}
.grounds-aside .grounds-since .info-i { margin-left: auto; }

/* Compliance is evicted from the Grounds (ruling 3) — the notice now lives on
 * the Charter (character-side) and the Register (facility-side). Both get a
 * little breathing room from the surrounding chapter content. */
.charter-compliance { margin: 4px 0 14px; }
.register-compliance { margin: 0 0 16px; }
/* Two-homes hint line: which fix this surface offers (§5.9(5)). Quiet, italic,
 * sits under the issue list. */
.compliance-hint {
  margin: 8px 0 0;
  font-family: var(--bb-font-italic);
  font-style: italic;
  font-size: 0.8125rem;
  line-height: 1.35;
  color: var(--bb-ink-faint);
}

/* The canvas pane — the hero column inside .grounds-body; scrolls on its own.
 * min-width:0 lets it shrink (not the fixed-width aside) when space is tight. */
.chapter-grounds .stage {
  flex: 1 1 auto;
  min-width: 0;
  min-height: 0;
  /* #230 pt1 restructure: .stage is the positioning context + the fullscreen
     target, NOT the scroller. It stays overflow:visible so the floating dock can
     be an absolute HUD that truly floats (sticky-only fell into flow when the map
     fit the screen) and so Map display's upward popover isn't clipped. The inner
     .stage-scroll now does the scrolling. */
  position: relative;
  display: flex;
  flex-direction: column;
  overflow: visible;
}
/* The scroll container (#230 pt1): takes over .stage's former scrolling role;
   holds the spine + canvas. (It held the sticky command rail too until #275
   lifted that out to .grounds-frame, above the body.) */
.chapter-grounds .stage-scroll {
  flex: 1 1 auto;
  min-width: 0;
  min-height: 0;
  overflow: auto;
  padding: 0 22px 24px;
}

/* Chapter header band (§ codex standard): the bridged tab already names the
 * chapter, so there is no title row — the .chapter-head band is an optional
 * utility strip carrying a caption/counts (.chapter-head-text) plus right-aligned
 * chrome (.chapter-head-aside: cap-echo / view controls). Only chapters with such
 * payload render it (Grounds, Register); the rest lead with a .section-label. The
 * Charter is exempt entirely — its bastion-name masthead is the title. */
.chapter-head {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 16px;
  margin: 0 0 18px;
}
.chapter-head-text { min-width: 0; }
/* The Register's counts line carries the chapter's Help glyph. .chapter-sub is
 * a <p>, so without a row context the glyph orphans onto a line of its own
 * under the counts. renderRegister writes the <p> with textContent, so the
 * glyph cannot live inside it — it has to be a sibling laid out beside it. */
.chapter-register .chapter-head-text {
  display: flex;
  align-items: baseline;
  gap: 6px;
}
.chapter-head-aside {
  margin-left: auto;
  display: flex;
  align-items: center;
  gap: 12px;
  flex-wrap: wrap;
}
.chapter-sub {
  margin: 0 0 14px;
  color: var(--bb-ink-faint);
  font-size: 0.86rem;
}
.chapter-head .chapter-sub { margin: 0; }

/* Tier 2: subsection heading. A small-caps
 * ruled label, used even when a chapter has a single section. Action-bearing
 * heads (Lore, The Works, Stores) wrap it in a flex row that owns the rule. */
.section-label {
  font-family: var(--bb-font-display);
  font-size: 0.78rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--bb-ink-soft);
  margin: 22px 0 10px;
  padding-bottom: 4px;
  border-bottom: 1px solid var(--bb-rule);
}
/* Register head has no title — center the counts caption against the cap-echo box
 * so the band reads as a ledger-tally row: running count left, capacity gauge
 * right. Trim the band's bottom margin and pull the first section label up under
 * it — the default 22px section gap left a needless void between the gauge and
 * the Special Facilities heading. */
.chapter-register .chapter-head { align-items: center; margin-bottom: 8px; }
#register-view > .section-label:first-child { margin-top: 8px; }
/* Chapters whose head band is gone (Household, Chronicle) lead with their first
 * heading; pull it up tight under the tabs so all four content chapters start at
 * the same offset. Household leads with a bare .section-label; Chronicle's
 * sections each lead with a ruled .chronicle-works-head (a row shared with
 * Charter's lore head — scope the pull to .chronicle-col so Charter is untouched).
 * Applying to ALL chronicle heads is reorder-safe (the .is-live lift reshuffles
 * which section is first); the grid row-gap owns the between-section spacing. */
.chapter-household > .section-label:first-child { margin-top: 4px; }
.chronicle-col .chronicle-works-head { margin-top: 4px; }

/* THE CHAPTER'S TWO SHAPES (#296) — the full argument is in index.html's
 * .chronicle-col comment. `.is-leaf` and the narrow tier are the same single
 * column; `.is-spread` is the only grid, and it exists only at ≥1200px.
 *
 * Which shape applies is a CLASS SET IN JS (chronicle-view.js), not a media
 * query, because the question it answers — "are there weeks to page through?" —
 * is about the document and not the viewport. The media query below then
 * decides only whether a spread has room to open. */
.chronicle-col {
  display: flex;
  flex-direction: column;
  gap: 26px;
  max-width: 760px;
}
/* THE LEAF's reading order. DOM order leads with the week's page because that
 * is the SPREAD's reading order and the spread is where the work happens; on a
 * leaf there is no week's page, only the invitation to start one, and an
 * optional feature's pitch belongs under the content it is optional to. This
 * is the anti-nag improvement the redesign was measured on — the pitch stops
 * interrupting the middle of the chapter and becomes a colophon at its foot. */
.chronicle-col.is-leaf > #chronicle-works { order: 1; }
.chronicle-col.is-leaf > #chronicle-stores { order: 2; }
.chronicle-col.is-leaf > #chronicle-turns { order: 3; }
.chronicle-col.is-leaf > .chronicle-viewer-note { order: 4; }

/* THE SPREAD: fore-edge | the week's page | the facing page. 1200px is the
 * floor at which all three fit at their own measure (106 + 40 + 658 + 40 + 372
 * + the chapter's gutters); below it the three simply stack, the fore-edge
 * turning from a margin into a strip (see .chronicle-foredge).
 *
 * Left-aligned like the other chapters, deliberately not centred: a centred
 * spread put the Chronicle's left edge well inboard of every other page's
 * 22px gutter, and the fore-edge — which IS a left margin — would then sit in
 * a second, invisible one. */
@media (min-width: 1200px) {
  .chronicle-col.is-spread {
    display: grid;
    grid-template-columns: 106px minmax(0, 1fr) minmax(320px, 372px);
    column-gap: 40px;
    row-gap: 26px;
    align-items: start;
    max-width: 1216px;
  }
  .chronicle-col.is-spread > #chronicle-foredge { grid-column: 1; grid-row: 1 / span 2; }
  .chronicle-col.is-spread > #chronicle-turns { grid-column: 2; grid-row: 1 / span 2; }
  .chronicle-col.is-spread > #chronicle-works { grid-column: 3; grid-row: 1; }
  .chronicle-col.is-spread > #chronicle-stores { grid-column: 3; grid-row: 2; position: relative; }
  /* The Reckoning stands in for BOTH facing sections, so it takes both rows —
   * a sealed week's account is one page, not a section stacked on a gap. */
  .chronicle-col.is-spread > #chronicle-reckoning {
    grid-column: 3;
    grid-row: 1 / span 2;
    position: relative;
  }
  .chronicle-col.is-spread > .chronicle-viewer-note { grid-column: 1 / -1; }
  /* The spine — a pseudo-element so the facing page's track width stays honest,
   * and the line ends where the page ends (no orphan rule over blank
   * parchment). It hangs off whichever section is currently facing. */
  .chronicle-col.is-spread > #chronicle-stores::before,
  .chronicle-col.is-spread > #chronicle-reckoning::before {
    content: "";
    position: absolute;
    left: -20px;
    top: 0;
    bottom: 0;
    width: 1px;
    background: var(--bb-rule-soft);
  }
  /* When the reader has turned back, Stores is hidden and the Reckoning owns
   * the spine; when Stores is hidden for any other reason it must not leave a
   * rule behind. `[hidden]` is display:none, so its ::before never paints —
   * this is stated rather than relied on silently, because the spine is drawn
   * by two selectors and only one of them can be right at a time. */

  /* THE FORE-EDGE BECOMES A MARGIN. Only here: the strip is the base shape
     (see .chronicle-foredge), and this is the one layout with a margin to put
     it in. Tabs read toward the page — right-aligned content, the rule on the
     right — because that is the edge the reader's eye travels back to. */
  .chronicle-col.is-spread > .chronicle-foredge {
    flex-direction: column;
    overflow-x: visible;
    scrollbar-gutter: auto;
    border-bottom: none;
    margin-top: 4px;
    /* The strip's end-fade has no business in a margin (#311): left on, it
       faded the right 24px of every right-aligned label and made the gutter
       undrawable — a shipped defect, found when the rail needed the gutter. */
    mask-image: none;
    /* The strip's padding-top made room for the rail above the tabs; here the
       rail runs beside them. */
    padding: 0;
  }
  .chronicle-col.is-spread .foredge-tab,
  .chronicle-col.is-spread .foredge-more {
    align-items: flex-end;
    text-align: right;
    border-bottom: 2px solid transparent;
    border-right: 2px solid var(--bb-rule-soft);
    padding: 0.28rem 0.5rem 0.3rem 0.2rem;
  }
  .chronicle-col.is-spread .foredge-tab.is-open {
    border-bottom-color: transparent;
    border-right-color: var(--bb-vermillion);
  }
  /* #310. The margin is 106px wide, and the longest SHIPPED label now fits it:
     the design pass measured a clip on "The Feast of the Moon", but the
     Forgotten Realms source names that festival "Feast of the Moon" and the
     catalog was corrected to match. The rule stays anyway, because it is not a
     rule about one festival — a homebrew calendar may carry any month name at
     all, and a clipped month is unreadable where a wrapped one is merely
     thicker, which in a fore-edge reads as a fore-edge rather than as a bug. It
     disturbs neither .is-open's right border nor .is-live: both are edge-drawn
     and indifferent to tab height. The strip keeps `nowrap` from the base rule
     and scrolls instead. */
  .chronicle-col.is-spread .foredge-lab {
    white-space: normal;
    overflow: visible;
    text-overflow: clip;
  }
  /* The words turn with the label (#312): the margin's cut, two lines,
     right-set, wrapping — already cut on a word inside the budget two lines
     hold, so no clamp is needed and none is drawn. */
  .chronicle-col.is-spread .fe-words {
    max-width: 100%;
    white-space: normal;
    overflow-wrap: anywhere;
  }
  .chronicle-col.is-spread .fe-words::before { content: attr(data-margin); }
  /* The gathering mark turns with the index: the run is vertical here, so the
     doubled hairline is horizontal, on the bottom edge facing the older week,
     and it reaches the rule on the right. */
  .chronicle-col.is-spread .foredge-tab.is-gathering::after {
    right: -2px;
    left: 45%;
    top: auto;
    bottom: -2px;
    width: auto;
    height: 3px;
    border-left: none;
    border-right: none;
    border-top: 1px solid var(--bb-rule-soft);
    border-bottom: 1px solid var(--bb-rule-soft);
  }
  /* THE RAIL TURNS WITH THE INDEX (#311): ruled DOWN the gutter, 19px past the
     tabs' rule, so the two read as two registers (the index's rule and the
     campaign's spine) and never as one smudged line — which is what absorbing
     the mark into the tab's border box produced, and why the mark overflows
     the box instead. Every geometry below mirrors a strip rule in the
     fore-edge section; the strip is the base shape and this is its turn. */
  .chronicle-col.is-spread .foredge-tab::before,
  .chronicle-col.is-spread .foredge-more::before,
  .chronicle-col.is-spread .fe-founded::before {
    left: auto;
    right: -19px;
    top: 0;
    /* Through the tab's 2px bottom border, or the line breaks at every tab. */
    bottom: -2px;
    width: 1px;
    height: auto;
  }
  .chronicle-col.is-spread .fe-mark {
    left: auto;
    margin-left: 0;
    right: -22px;
    top: 50%;
    margin-top: -3.5px;
  }
  .chronicle-col.is-spread .foredge-more .fe-seg {
    left: auto;
    right: -21px;
    top: 3px;
    bottom: 3px;
    width: 5px;
    height: auto;
  }
  .chronicle-col.is-spread .fe-founded {
    margin: 0;
    padding: 10px 0.5rem 0 0.2rem;
    text-align: right;
    white-space: normal;
    /* TRANSPARENT, and load-bearing. Every rail offset above is a `right:`,
       which resolves against the PADDING box — and each tab's padding box is
       inset by the 2px border that draws the index's own rule. The foot has no
       rule by design (the tab rule visibly stops above it), so without a
       matching border its padding box is 2px wider, the last rail segment and
       the cross-stroke land 2px right of the line above them, and the terminus
       reads as off-centre. Matching the box keeps one vocabulary for the
       offsets instead of a second set of numbers that mean the same thing. */
    border-right: 2px solid transparent;
  }
  .chronicle-col.is-spread .fe-founded::before { top: 0; bottom: auto; height: 10px; }
  .chronicle-col.is-spread .fe-founded::after {
    left: auto;
    right: -23px;
    top: 10px;
    width: 9px;
    height: 1px;
  }
}
/* A shared snapshot (#254) is always a leaf, so it never reaches the grid above
 * — Works and the Turns page are history and stay home, and a spread whose left
 * page is blank would rule a spine down the middle of a one-page book. Nothing
 * to undo: `.is-leaf` is what chronicle-view.js returns for a read-only
 * session, and the leaf is the single column every narrow screen already uses. */
.chronicle-section { display: flex; flex-direction: column; }
/* The chapter's closing boundary in a read-only snapshot (#272): what a
   snapshot is, not an error and not an invitation. Same fact register as
   .annotation-legend-hint / .fig-note, with a dotted rule above so it reads as
   a closing note rather than a stranded paragraph.

   No `grid-column` needed, and that's worth stating because it looks like an
   omission: this note only ever exists in a viewer, and `body.viewer
   .chronicle-col` above already collapses the ≥1200px codex spread back to a
   single flex column — so there is no facing rail for it to fall into. */
.chronicle-viewer-note {
  margin: 0;
  padding-top: 14px;
  border-top: 1px dotted var(--bb-rule-soft);
  font-family: var(--bb-font-italic);
  font-style: italic;
  font-size: 0.8rem;
  color: var(--bb-ink-soft);
}
/* Section descriptor under a Chronicle section-label (faint, like .chapter-sub). */
.chronicle-section-sub {
  margin: 0 0 12px;
  color: var(--bb-ink-faint);
  font-size: 0.86rem;
}

/* Autosave indicator — italic marginalia in the manuscript voice (§3.4).
 * The autosave note in the top bar's left cluster (§5.2) — quiet italic,
 * centered on the band's optical line. */
.save-status {
  font-family: var(--bb-font-italic);
  font-style: italic;
  font-size: 0.8125rem;
  color: var(--bb-ink-soft);
  display: inline-flex;
  align-items: center;
  line-height: 1;
  white-space: nowrap;
  user-select: none;
}
.save-status.pending { color: var(--bb-vermillion); }
/* Quota-full autosave failure — persistent, not a transient toast state. */
.save-status.save-error { color: var(--bb-vermillion); font-weight: 600; }

/* ---- Bastions ▾ menu (Codex §4) ----------------------------------------- */

.bastions-menu-wrap { position: relative; }

.bastions-menu {
  position: absolute;
  top: calc(100% + 4px);
  left: 0;
  margin: 0;
  padding: 4px 0;
  background: var(--bb-parchment-light);
  border: 1px solid var(--bb-rule);
  border-radius: 0;
  box-shadow: 0 8px 18px -6px var(--bb-shadow);
  min-width: 340px;
  max-height: 60vh;
  overflow-y: auto;
  z-index: 40;
}

/* Saves menu only (the class is shared with the Grounds "Map display ▾" panel,
 * which stays a single scrolling block): the bastion list scrolls inside the
 * menu while New / Load sample / the sync line stay put. Before this they were
 * "pinned" only by being last in the DOM, so a library of seventeen pushed them
 * out of reach — the one thing a switcher must never do. */
#bastions-menu {
  display: flex;
  flex-direction: column;
  overflow: hidden;
}
#bastions-menu .bastions-menu-list {
  min-height: 0;
  overflow-y: auto;
  scrollbar-gutter: stable;
  padding-right: 12px;
}
#bastions-menu .bastions-menu-foot { flex: 0 0 auto; }

.bastions-menu-row {
  display: grid;
  /* Just the name now — clone / export / share / delete moved to home in #264,
     and the four `auto` tracks they used to occupy went with them. */
  grid-template-columns: 1fr;
  align-items: stretch;
  background: transparent;
}
.bastions-menu-row:hover { background: var(--bb-parchment-mid); }

.bastions-menu-row .bm-load {
  display: flex;
  align-items: center;
  gap: 6px;
  width: 100%;
  text-align: left;
  padding: 8px 14px;
  background: transparent;
  border: none;
  border-radius: 0;
  color: var(--bb-ink);
  font-family: var(--bb-font-display);
  font-size: 0.875rem;        /* 14px */
  font-weight: 500;
  letter-spacing: 0.02em;
  cursor: pointer;
  box-shadow: none;
  min-width: 0;
}
.bastions-menu-row.current .bm-load { color: var(--bb-vermillion-deep); }

/* The verb buttons left this menu in #264 — duplicate / export / share / delete
 * live on home now, which is the authoritative library surface. What remains
 * here is the switcher: the current-dot, the name, and the row itself. The
 * .bm-action family below is therefore scoped to .holding ALONE; it is no
 * longer a shared vocabulary, which is also the end of the drift that produced
 * two spellings of Duplicate and two share glyphs. */
.bastions-menu-row .bm-dot,
.holding .bm-dot {
  display: inline-block;
  width: 0.8em;
  color: var(--bb-vermillion);
  font-size: 0.8em;
  flex: 0 0 auto;
}
/* Wraps rather than ellipsising (#264): home promises names are never
 * truncated, and this switcher is one click away. Menu rows growing to fit a
 * long name is correct behaviour for a menu. */
.bastions-menu-row .bm-name {
  white-space: normal;
  overflow-wrap: anywhere;
}

.holding .bm-action {
  background: transparent;
  border: none;
  border-radius: 0;
  color: var(--bb-ink-soft);
  width: 28px;
  height: 28px;
  padding: 0;
  font-size: 0.95rem;
  cursor: pointer;
  box-shadow: none;
  display: flex;
  align-items: center;
  justify-content: center;
  transition: color 80ms ease, background-color 80ms ease;
}
/* Every verb hovers, including share and the overflow — on home the buttons are
 * always visible and hover is the primary affordance, so an inert one reads as
 * broken. (.bm-share had no hover rule at all while it lived in the dropdown;
 * the gap was invisible there and obvious here.) */
.holding .bm-clone:hover,
.holding .bm-export:hover,
.holding .bm-share:hover,
.holding .bm-more:hover { color: var(--bb-vermillion); background: transparent; }

/* Pinned bottom rows (`+ New bastion`, `↥ Import from file`). The divider
 * above the pinned section is drawn once, on the first .new row only —
 * the second .new row suppresses it via the adjacent-sibling selector. */
.bastions-menu-row.new {
  display: block;
  width: 100%;
  text-align: left;
  padding: 8px 14px;
  background: transparent;
  border: none;
  border-top: 1px solid var(--bb-rule-soft);
  border-radius: 0;
  color: var(--bb-ink-soft);
  font-family: var(--bb-font-italic);
  font-style: italic;
  font-size: 0.875rem;        /* 14px */
  cursor: pointer;
  box-shadow: none;
}
.bastions-menu-row.new + .bastions-menu-row.new { border-top: 0; }
.bastions-menu-row.new:hover {
  background: var(--bb-parchment-mid);
  color: var(--bb-ink);
}

/* Cloud-sync status colophon (#216) — a read-only footer line in the Bastions
 * menu when cloud sync is connected. Not a menuitem: no hover, no focus, no click. */
.bastions-menu-colophon {
  padding: 8px 14px;
  border-top: 1px solid var(--bb-rule-soft);
  font-family: var(--bb-font-italic);
  font-style: italic;
  font-size: 0.8125rem;
  color: var(--bb-ink-faint);
  cursor: default;
}

/* Context menu (#190) — right-click a facility rect or tray chip in Select &
 * Move mode. Reuses the .bastions-menu recipe verbatim: parchment fill, square
 * corners, ink rule, hover fill, no glyph discs. position:fixed; JS sets
 * left/top from the cursor (createContextMenu in dom.js). */
.context-menu {
  position: fixed;
  z-index: 60;
  margin: 0;
  padding: 4px 0;
  list-style: none;
  min-width: 220px;
  max-width: min(320px, 90vw);
  background: var(--bb-parchment-light);
  border: 1px solid var(--bb-rule);
  border-radius: 0;
  box-shadow: 0 8px 18px -6px var(--bb-shadow);
}
.context-menu .ctx-li { margin: 0; }
.context-menu .ctx-li.ctx-divider {
  border-top: 1px solid var(--bb-rule-soft);
  margin-top: 4px;
  padding-top: 4px;
}
.context-menu .ctx-item {
  display: flex;
  align-items: center;
  gap: 8px;
  width: 100%;
  text-align: left;
  padding: 8px 14px;
  background: transparent;
  border: none;
  border-radius: 0;
  color: var(--bb-ink);
  font-family: var(--bb-font-display);
  font-size: 0.875rem;        /* 14px */
  font-weight: 500;
  letter-spacing: 0.02em;
  cursor: pointer;
  box-shadow: none;
}
.context-menu .ctx-item:hover { background: var(--bb-parchment-mid); }
.context-menu .ctx-item:focus-visible {
  outline: 2px solid var(--bb-vermillion);
  outline-offset: -2px;
}
.context-menu .ctx-label { flex: 1 1 auto; }
.context-menu .ctx-lead { flex: 0 0 auto; width: 1.1em; text-align: center; }
.context-menu .ctx-trail { flex: 0 0 auto; color: var(--bb-ink-soft); }
.context-menu .ctx-shortcut {
  flex: 0 0 auto;
  margin-left: auto;
  padding: 1px 5px;
  font-size: 0.72rem;
  color: var(--bb-ink-faint);
  border: 1px solid var(--bb-rule-soft);
  border-radius: 3px;
  font-variant-numeric: tabular-nums;
}
.context-menu .ctx-danger:hover .ctx-shortcut { color: inherit; border-color: currentColor; }
.context-menu .ctx-item[aria-disabled="true"] { color: var(--bb-ink-faint); cursor: default; }
.context-menu .ctx-item[aria-disabled="true"]:hover { background: transparent; }
/* Destructive Remove row — the danger ground with knocked-out light ink, the
   one place a menu row inverts rather than just recolouring its text. */
.context-menu .ctx-danger:hover { color: var(--bb-parchment-light); background: var(--bb-danger); }
.context-menu .ctx-danger:hover .ctx-lead,
.context-menu .ctx-danger:hover .ctx-trail { color: inherit; }
/* Cross-nav anchor row ("Manage in Register →") — italic, never a danger. */
.context-menu .ctx-anchor {
  color: var(--bb-ink-soft);
  font-family: var(--bb-font-italic);
  font-style: italic;
}
.context-menu .ctx-anchor:hover { color: var(--bb-ink); }

/* Right-clicked tray chip echo, mirroring .facility-group.rail-lit on the map. */
.tray-chip.ctx-target { outline: 2px solid var(--bb-vermillion); outline-offset: 1px; }

/* Register card echo (#201) — same vocabulary at card scale, marking which
 * card the open context menu acts on. */
.reg-card.ctx-target { outline: 2px solid var(--bb-vermillion); outline-offset: 2px; }

/* Overflow "⋯ More" menu (#105) — phones only. Hidden on desktop; the phone
 * tier (bottom of file) flips .more-menu-wrap to inline-block. Reuses the
 * .bastions-menu popover chrome, anchored to the right so it doesn't overflow
 * the viewport near the screen edge. Rows are plain text items (no .bm-load
 * child), so they carry their own padding/typography. */
.more-menu-wrap { position: relative; display: none; }
#more-menu {
  left: auto;
  right: 0;
  min-width: 220px;
  max-width: calc(100vw - 24px);
}
#more-menu .bastions-menu-row {
  display: block;
  width: 100%;
  text-align: left;
  padding: 12px 14px;         /* ~44px touch row, matching the pointer:coarse min */
  background: transparent;
  border: none;
  border-radius: 0;
  color: var(--bb-ink);
  font-family: var(--bb-font-display);
  font-size: 0.875rem;        /* 14px — parity with the Bastions/History rows */
  font-weight: 500;
  letter-spacing: 0.02em;
  cursor: pointer;
  box-shadow: none;
}
#more-menu .bastions-menu-row:hover { background: var(--bb-parchment-mid); }
/* Visible ring for the row auto-focused on open (rows are full-bleed → inset). */
#more-menu .bastions-menu-row:focus-visible {
  outline: 2px solid var(--bb-vermillion);
  outline-offset: -2px;
}

/* ---- Open handout — top-bar primary CTA (§3.5) -------------------------- */

.btn-handout {
  white-space: nowrap;
  font-family: var(--bb-font-display);
  font-size: 0.84rem;       /* 13.5px — unified with .btn-ghost */
  letter-spacing: 0.14em;
  text-transform: uppercase;
  padding: 6px 14px;
}
/* btn-primary itself is restyled to the gilt CTA treatment in §7 modal work
 * later; for slice 2 we re-anchor it here on the handout button only. */
button.btn-primary.btn-handout {
  background: linear-gradient(180deg, var(--bb-gilt-bright), var(--bb-gilt));
  color: var(--bb-ink);
  border: 1px solid var(--bb-gilt-deep);
  font-weight: 600;
  box-shadow:
    0 1px 0 var(--bb-inset-strong) inset,
    0 2px 4px var(--bb-shadow);
}
button.btn-primary.btn-handout:hover {
  background: linear-gradient(180deg,
      color-mix(in oklch, var(--bb-gilt-bright), white 6%),
      var(--bb-gilt-bright));
  border-color: var(--bb-gilt-deep);
}

/* ---- Bastions ▾ hover-preview tooltip (#53 / §4.3) ---------------------- */

.bastion-preview {
  position: fixed;
  pointer-events: none;
  background: var(--bb-parchment-light);
  border: 1px solid var(--bb-rule);
  border-radius: 0;
  box-shadow: 0 8px 18px -6px var(--bb-shadow);
  padding: 6px;
  z-index: 50;
  max-width: 360px;
  max-height: 70vh;
  overflow: hidden;
}
.bastion-preview svg { display: block; pointer-events: none; }

/* Compliance notice — persistent non-compliant state banner (#83). */
.compliance-notice {
  padding: 10px 12px;
  background: var(--bb-parchment-mid);
  border: 1px solid var(--bb-gilt-deep);
  border-left: 3px solid var(--bb-gilt);
  box-shadow: var(--shadow-sunken);
  gap: 0;
}
.compliance-head {
  display: flex;
  align-items: center;
  gap: 6px;
  margin-bottom: 6px;
}
.compliance-head h3 {
  margin: 0;
  font-family: var(--bb-font-display);
  font-size: 0.875rem;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--bb-gilt-deep);
}
.compliance-glyph {
  width: 16px;
  height: 16px;
  flex-shrink: 0;
  color: var(--bb-gilt-deep);
}
.compliance-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 4px;
}
.compliance-item {
  font-family: var(--bb-font-italic);
  font-size: 0.875rem;
  line-height: 1.35;
  color: var(--bb-ink-soft);
}
.compliance-item::before {
  content: "— ";
  color: var(--bb-gilt-deep);
}
/* The Register's half of the banner (#300): one chip per breach instead of a
   stack of sentences. The sentence still exists — it rides the chip's data-tip
   and is printed in full in the leaf the chip opens — but 23 chambers' worth of
   repeated prose was pushing the index off the screen, and on this chapter the
   useful question is WHICH chamber, not what the rule says. The Charter keeps
   sentences: its fix is on the character, and the sentence names the character. */
.compliance-count {
  font-family: var(--bb-font-italic);
  font-size: 0.8rem;
  color: var(--bb-ink-faint);
}
.compliance-chips {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
}
.compliance-chip {
  font-family: var(--bb-font-display);
  font-size: 0.78rem;
  letter-spacing: 0.02em;
  padding: 3px 9px;
  border: 1px solid var(--bb-vermillion);
  border-radius: 0;
  box-shadow: none;
  background: var(--bb-parchment-light);
  color: var(--bb-vermillion-deep);
  cursor: pointer;
}
.compliance-chip:hover { background: var(--bb-gilt-fill); }
.compliance-chip:focus-visible {
  outline: 2px solid var(--bb-vermillion);
  outline-offset: 2px;
}
/* A breach with no chamber to open — an owner over their cap, a helm that was
   never built — is DEAD on purpose. Dashed and unclickable says "there is
   nowhere to go from here", which is the honest answer; a live-looking chip
   that did nothing would be worse than no chip. */
.compliance-chip.is-inert {
  border-style: dashed;
  border-color: var(--bb-ink-faint);
  color: var(--bb-ink-faint);
  background: transparent;
  cursor: default;
}

/* Decorative fleuron under the identity block (§5.1). */
.fleuron {
  display: block;
  margin: 6px auto 0;
  fill: var(--bb-gilt);
  opacity: 0.85;
}

/* ---- Bastion name overlay (Charter, §5.4) ------------------------------- */
/* The read-mode overlay is in-flow and sizes the wrapper (it may wrap to a
 * second line); the edit input stretches over it absolutely. On :focus-within
 * the overlay hides — keeping its layout box — and the input takes over at the
 * same display size. (The `.charter-identity`-scoped wrap / input / drop-cap
 * rules live in the Charter block; this is the shared display base.) */
.bastion-name-display {
  display: block;
  font-family: var(--bb-font-display);
  font-size: 1.75rem;           /* 28px — matches the input's edit-state size */
  font-weight: 500;
  letter-spacing: 0.01em;
  color: var(--bb-ink);
  line-height: 1.15;
  padding: 12px 9px 0 9px;      /* 9px x-inset = input's 8px padding + 1px border */
  min-height: 56px;             /* room for the 56px drop cap on one-liners */
  max-height: calc(12px + 2.3em); /* clip past two lines (no clamp — it would kill ::first-letter) */
  overflow: hidden;
  pointer-events: none;
  user-select: none;
}
/* ---- Heraldry crest slot (#123) ----------------------------------------- */

.crest-wrap { position: relative; width: 64px; justify-self: end; }
.crest-slot {
  display: block;
  width: 64px;
  height: 77px;                 /* 5:6 — heater-shield ratio, seal-sized for the corner */
  padding: 2px;
  background: transparent;
  border: 1px solid transparent; /* holds the box — real art sits frameless on the parchment */
  border-radius: 0;
  cursor: pointer;
  box-shadow: none;
}
.crest-slot img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: contain;
}
/* Hover/focus affordance is an outline halo, never a frame — a solid border
 * around full-color art read as a harsh black box on parchment. */
.crest-slot:not(.is-placeholder):hover,
.crest-slot:focus-visible {
  outline: 2px solid var(--bb-vermillion);
  outline-offset: 2px;
}
/* Empty state reads as a fillable ghost slot, not real art: dashed border
 * is the non-color cue, the washed-out placeholder the second. */
.crest-slot.is-placeholder { border: 1px dashed var(--bb-rule-soft); }
.crest-slot.is-placeholder img { opacity: 0.45; filter: grayscale(1); }
.crest-slot.is-placeholder:hover,
.crest-slot.is-placeholder:focus-visible { border-color: var(--bb-vermillion); }
.crest-slot.is-placeholder:hover img,
.crest-slot.is-placeholder:focus-visible img { opacity: 1; }

/* Upload/Replace/Remove menu — .bastions-menu chrome, narrow plain rows.
 * From a bottom-right host the shared down-left placement would overflow the
 * section bottom and the rail's right edge — open up + right-aligned. */
.crest-menu {
  min-width: 180px;
  top: auto;
  bottom: calc(100% + 4px);
  left: auto;
  right: 0;
}

.crest-menu .bastions-menu-row {
  display: block;
  width: 100%;
  text-align: left;
  padding: 8px 14px;
  background: transparent;
  border: none;
  border-radius: 0;
  color: var(--bb-ink);
  font-family: var(--bb-font-display);
  font-size: 0.875rem;
  cursor: pointer;
  box-shadow: none;
}
.crest-menu .crest-remove { border-top: 1px solid var(--bb-rule-soft); }
.crest-menu .crest-remove:hover {
  color: var(--bb-parchment-light);
  background: var(--bb-danger);
}

/* ---- Cap meter (special facilities) ------------------------------------- */

/* ---- Cap meter (§5.4) --------------------------------------------------- */

.cap-row {
  display: flex;
  flex-direction: column;
  gap: 6px;
  padding: 10px 12px;
  background: var(--bb-parchment-mid);
  border: 1px solid var(--bb-rule-soft);
  box-shadow: var(--shadow-sunken);
}
.cap-row .label-row {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  font-family: var(--bb-font-display);
  font-size: 0.75rem;         /* 12px */
  letter-spacing: 0.20em;
  text-transform: uppercase;
  color: var(--bb-ink-soft);
}
.cap-row .count-big {
  font-family: var(--bb-font-display);
  font-size: 1.0625rem;         /* 17px */
  color: var(--bb-vermillion-deep);
  letter-spacing: 0;
  text-transform: none;
  font-variant-numeric: lining-nums tabular-nums;
  white-space: nowrap;
}
.cap-row .meter {
  position: relative;
  height: 8px;
  background: var(--bb-parchment-dark);
  border: 1px solid var(--bb-rule-soft);
  border-radius: 0;
  overflow: hidden;
}
.cap-row .meter > .fill {
  position: absolute;
  inset: 0 auto 0 0;
  background: linear-gradient(90deg, var(--bb-gilt), var(--bb-gilt-bright));
  border-radius: 1px;           /* §2.4 exception: progress-bar fill caps */
}
.cap-row .next-up {
  font-family: var(--bb-font-italic);
  font-style: italic;
  font-size: 0.84rem;         /* 13.5px */
  color: var(--bb-ink-soft);
}
.cap-row .next-up .lvl {
  color: var(--bb-vermillion-deep);
  font-family: var(--bb-font-display);
  font-style: normal;
  font-weight: 600;
  font-variant-numeric: lining-nums tabular-nums;
}
#cap-per-char:not(:empty) {
  display: flex;
  flex-direction: column;
  gap: 3px;
  padding-bottom: 6px;
  border-bottom: 1px solid var(--bb-rule-soft);
}
.cap-char-row {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  font-size: 0.84rem;          /* 13.5px */
}
.cap-char-name {
  flex: 1 1 auto;
  color: var(--bb-ink-soft);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  min-width: 0;
}
.cap-char-count {
  flex: 0 0 auto;
  color: var(--bb-vermillion-deep);
  font-family: var(--bb-font-display);
  font-variant-numeric: lining-nums tabular-nums;
  white-space: nowrap;
  padding-left: 8px;
}

/* ---- Facility icon discs (§5.5) ----------------------------------------- */
/* dialog candidate rows, and the kind-chooser tiles (#169). ---------------- */

.glyph {
  width: 28px; height: 28px;
  display: grid; place-items: center;
  border: 1px solid var(--bb-rule);
  border-radius: 50%;            /* §2.4 exception: tinted icon disc */
  background: var(--bb-parchment-light);
  flex: 0 0 auto;
}
.glyph .glyph-svg {
  width: 18px; height: 18px;
  display: block;
  fill: currentColor;
}
.glyph.special {
  background: var(--bb-special-fill);
  color: var(--bb-vermillion-deep);
  border-color: rgba(168,40,29,0.35);
}
.glyph.basic {
  background: var(--bb-basic-fill);
  color: var(--bb-sapphire-deep);
  border-color: rgba(46,74,124,0.35);
}

/* Section headers (§5.4–5.7) ---------------------------------------------- */

.section-head {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
}
/* h2 shares the recipe (#264): home's outline is h1 → h2 per bay, and the
 * alternative was skipping a level or restyling. A selector widening, not a
 * second typography. */
.section-head h3,
.section-head h2 {
  margin: 0;
  font-family: var(--bb-font-display);
  font-size: 0.75rem;          /* 12px */
  font-weight: 600;
  letter-spacing: 0.20em;
  text-transform: uppercase;
  color: var(--bb-ink);
}

/* ---- Info glyph + the defenses line (§5.2 / §5.3) ----------------------- */
/* The `.stat-tile` rules that opened this section went with the Charter's
   figures grid (#301) — nothing renders that class any more. `.defenses-line`
   below outlives it, because #milestone-line still reuses the strip in the
   Chronicle; that is why the class stayed when the tiles did not. */

.info-i {
  display: inline-grid;
  place-items: center;
  width: 14px; height: 14px;
  border-radius: 50%;
  background: var(--bb-parchment-mid);
  border: 1px solid var(--bb-rule-soft);
  color: var(--bb-ink-faint);
  font-size: 0.6875rem;
  font-family: var(--bb-font-display);
  font-style: normal;
  vertical-align: 2px;
  margin-left: 4px;
  cursor: pointer;
}
.info-i:hover {
  color: var(--bb-ink);
  border-color: var(--bb-rule);
}

/* Anything carrying a Help deep-link (#117) is clickable — info-i glyphs,
 * the enclosed-defenses badge, owner chips, the type badge. */
[data-help-section] { cursor: pointer; }

/* Defensive-walls summary line — its own parchment-mid recessed strip
 * below the stats pair (§5.3). Hidden until at least one defensive-wall
 * cell is painted. */
.defenses-line {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 8px 10px;
  margin-top: 10px;
  padding: 8px 12px;
  background: var(--bb-parchment-mid);
  border: 1px solid var(--bb-rule-soft);
  border-radius: 0;
  font-family: var(--bb-font-display);
  font-size: 0.875rem;           /* 14px */
  color: var(--bb-ink);
}
.defenses-line .defenses-label {
  font-family: var(--bb-font-display);
  font-size: 0.6875rem;           /* 11px */
  letter-spacing: 0.20em;
  text-transform: uppercase;
  color: var(--bb-ink-faint);
}
.defenses-line .defenses-stats {
  color: var(--bb-ink);
  font-variant-numeric: lining-nums tabular-nums;
}
.defenses-line .defenses-stats .unit {
  font-family: var(--bb-font-italic);
  font-style: italic;
  color: var(--bb-ink-faint);
  margin-left: 1px;
}
/* The share + reader strips' own rules lived here (#271/#266). Both moved into
   the Charter's colophon in #301 and are drawn by .sv-cell / .sv-verb now; the
   .fig-turn-link overrides they needed went with the tile grammar. */
.defenses-badge {
  margin-left: auto;
  padding: 3px 8px;
  border-radius: 999px;          /* §2.4 exception: badge pill */
  background: linear-gradient(180deg,
      rgba(74,111,58,0.28),
      rgba(74,111,58,0.18));
  border: 1px solid rgba(74,111,58,0.45);
  color: var(--bb-verdigris-deep);
  font-family: var(--bb-font-display);
  font-size: 0.6875rem;           /* 11px */
  letter-spacing: 0.06em;
  text-transform: uppercase;
}

/* ---- Right pane: the floorplan as hero ---------------------------------- */

/* ---- Stage container (Codex §6) ----------------------------------------- */

.stage {
  /* Scrolling + padding moved to the inner .stage-scroll (#230 pt1); .stage is
   * now the relative wrapper (see .chapter-grounds .stage). Only the faint
   * vignette ground stays here so it fills the whole stage incl. fullscreen. */
  background:
    radial-gradient(ellipse 60% 40% at 50% 0%,
        color-mix(in srgb, var(--bb-parchment-mid) 55%, transparent) 0%,
        transparent 60%);
}

/* Fullscreen floorplan (#121; target moved to .grounds-frame in #275): the
   opaque parchment ground lives on <body>, which isn't rendered while the frame
   is promoted to the top layer. Without an opaque base here the default black
   ::backdrop shows through. Set just the base colour (per-theme via
   --bb-parchment) so each theme's top-glow gradient is preserved.
   The frame — not .stage — is the target because the command rail now sits
   outside .stage; targeting .stage would put the tools outside fullscreen. */
.grounds-frame:fullscreen { background-color: var(--bb-parchment); }
.grounds-frame:fullscreen::backdrop { background: var(--bb-parchment); }

/* ---- Placement banner — active/placing state (§6.1) --------------------- */

/* Idle drops the banner entirely (R2.3); display:flex below would otherwise
 * win over the [hidden] attribute, so guard it explicitly. */
.placement-banner[hidden] { display: none; }
.placement-banner {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 14px;
  /* Row 2 of .grounds-command — the rail's padding + the toolbar's
   * margin-bottom set the spacing, so the status zone carries no outer margin
   * of its own (a stray margin here would reintroduce reflow). */
  margin: 0;
  background: linear-gradient(180deg,
      color-mix(in srgb, var(--bb-gilt-bright) 18%, transparent),
      color-mix(in srgb, var(--bb-gilt-bright) 10%, transparent));
  border: 1px solid var(--bb-gilt);
  border-radius: 0;
  color: var(--bb-ink);
  box-shadow:
    0 1px 0 var(--bb-inset-light) inset,
    0 6px 14px -10px var(--bb-hairline);
  animation: banner-pulse 2.2s ease-in-out infinite;
}
@keyframes banner-pulse {
  0%, 100% {
    box-shadow:
      0 1px 0 var(--bb-inset-light) inset,
      0 6px 14px -10px var(--bb-hairline);
  }
  50% {
    box-shadow:
      0 1px 0 var(--bb-inset-light) inset,
      0 8px 22px -6px color-mix(in srgb, var(--bb-gilt-bright) 55%, transparent);
  }
}
@media (prefers-reduced-motion: reduce) {
  .placement-banner { animation: none; }
}

.placement-banner .pb-icon {
  flex: 0 0 22px;
  width: 22px; height: 22px;
  display: grid; place-items: center;
  border-radius: 50%;            /* §2.4 exception: icon disc */
  background: var(--bb-vermillion);
  color: var(--bb-parchment-light);
  font-family: var(--bb-font-display);
  font-weight: 600;
  font-size: 0.875rem;
}
.placement-banner .pb-text {
  display: flex;
  align-items: baseline;
  gap: 10px;
  min-width: 0;
}
.placement-banner .pb-text strong {
  font-family: var(--bb-font-display);
  font-size: 0.875rem;           /* 14px */
  font-weight: 600;
  color: var(--bb-ink);
}
.placement-banner .pb-text small {
  font-family: var(--bb-font-italic);
  font-style: italic;
  font-size: 0.84rem;          /* 13.5px */
  color: var(--bb-ink-soft);
  letter-spacing: 0;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.placement-banner .pb-text small.bad { color: var(--bb-danger); }
.placement-banner .spacer { flex: 1; }
/* Help deep-link glyph — quiet chrome at the banner's trailing edge,
 * shown only while a tool mode is active (#117). */
.placement-banner .pb-help {
  flex: none;
  margin: 0 2px;
  vertical-align: 0;
}

/* Tool-mode chip — gilt-deep pill (§6.1 tool-mode state). Gilt, not red:
 * a selected tool is a current/active state, not an alert (#131 — red is
 * reserved for over-cap/compliance). Pairs with the gilt pb-icon disc as one
 * gold "active tool" cluster. parchment-light text clears AA on gilt-deep
 * (5.98:1 default; Arcane has no gilt luminous flip so it gets an override
 * below). The chip must stay one line at narrow widths (tuned in #40 — do
 * not regress). */
.placement-banner.mode #placement-status {
  flex: 0 0 auto;
  padding: 4px 12px;
  border-radius: 999px;          /* §2.4 exception: status pill */
  background: var(--bb-gilt-deep);
  color: var(--bb-parchment-light);
  font-family: var(--bb-font-display);
  font-size: 0.75rem;          /* 12px */
  font-weight: 600;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  white-space: nowrap;
}
/* Tool-mode pb-icon: gilt-filled disc with a hand-drawn line-art SVG
 * glyph (§2.5 + §6.1, decision #3). The path inside the SVG file
 * carries stroke="currentColor" so the disc's color drives the ink. */
.placement-banner.mode .pb-icon {
  background: var(--bb-gilt);
  color: var(--bb-parchment-light);
}
.placement-banner .pb-icon .glyph-svg {
  width: 14px;
  height: 14px;
  display: block;
}

/* Cancel button — vermillion text on transparent, fill on hover (§6.1). */
.placement-banner #btn-cancel-placement {
  background: transparent;
  border: 1px solid var(--bb-vermillion);
  color: var(--bb-vermillion);
  box-shadow: none;
  font-family: var(--bb-font-display);
  font-size: 0.75rem;          /* 12px */
  letter-spacing: 0.14em;
  text-transform: uppercase;
  padding: 5px 12px;
}
.placement-banner #btn-cancel-placement:hover {
  background: var(--bb-vermillion);
  color: var(--bb-parchment-light);
  border-color: var(--bb-vermillion);
}

/* ---- Grid controls (Codex §6.2) ----------------------------------------- */

/* Grid controls cluster. One instance (#230 pt1): the canvas view-dock, which
 * is where Map display ▾ lives; Export map ▾ sits in the app top bar. This
 * base rule styles it; the dock rule below adds the float
 * slab + positioning. */
.grid-ctrls {
  display: flex;
  align-items: center;
  gap: 10px;
}

/* Canvas view-dock (#230 pt1) — the ephemeral-view instruments (grid-dims, zoom,
 * fullscreen, ghost, Fit, Overview) + Map display ▾ as a quiet translucent slab
 * floating on the map, bottom-left. It carries .grid-ctrls so every existing
 * view-control button keeps its chrome; the float slab + absolute HUD positioning
 * are on this rule (see the inner note). The translucency is deliberately lighter
 * than the command rail so the dock reads as "laid on the map," not part of the
 * chrome. */
.grounds-view-dock {
  /* Absolute HUD on .stage (position:relative), OUTSIDE .stage-scroll — so it
   * floats over the canvas viewport at every zoom, in both the fits-the-screen
   * and zoomed-and-panning cases (the earlier sticky version fell into flow with
   * no scroll). Float inward over the grid vellum, inset from the plate's
   * bottom-left neatline — never straddling it. left:150 clears the spine
   * (~116px) + plate frame; bottom:20 lifts off the bottom neatline. The
   * drop-shadow carries the depth. Tune against the live rendered edges. */
  position: absolute;
  left: 150px;
  bottom: 20px;
  z-index: 30;                  /* below modals; see note on the rail's 20 */
  width: fit-content;
  /* Hardening (#275). An absolute HUD on an overflow:visible .stage can paint
   * straight across the library rail — at 800px it was 665px of controls in a
   * 478px pane, spilling 281px over the rail and covering the milestone strip.
   * The cap makes that structurally impossible, and wrapping absorbs the
   * overflow instead. Extra rows grow UPWARD from the bottom:20px anchor, into
   * the canvas and away from the plate's bottom neatline — the right direction.
   * 170px = left:150 + a 20px right inset; the containing block is .stage's box
   * (its padding lives on the inner .stage-scroll). */
  max-width: calc(100% - 170px);
  flex-wrap: wrap;
  gap: 6px 8px;
  padding: 5px 8px;
  border-radius: 0;             /* honor the square handout grammar */
  background: color-mix(in srgb, var(--bb-parchment-light) 82%, transparent);
  backdrop-filter: blur(8px) saturate(1.04);
  -webkit-backdrop-filter: blur(8px) saturate(1.04);
  border: 1px solid var(--bb-rule-soft);
  /* Real elevation is the "floating" cue — markedly stronger than a flat plaque. */
  box-shadow:
    0 4px 16px -6px rgba(60,30,10,0.40),
    0 1px 0 var(--bb-inset-light) inset;
}
/* The dock's own hairline seams cannot survive its new wrap (#275) — the same
 * rule that retired .tool-seam from the toolbar, and the dock is no longer the
 * exception it was, because it now wraps too. A seam landing at a row edge
 * draws a dangling divider with no CSS to suppress it.
 * Measured max-content by display face: codex 665 · cartographer 661 · verdant
 * 649 · arcane 647. Wrap-free needs stage ≥ worst + 170 (the left:150 inset plus
 * a 20px right margin), i.e. a ~1157px window with the rail and seam present —
 * so seams are safe only well above the tier boundary. 1200 gives the widest
 * face ~43px of headroom; below it the 8px flex gap separates the clusters,
 * which is what the toolbar does too. */
@media (max-width: 1200px) {
  .grounds-view-dock .grid-ctrls-seam { display: none; }
}

/* Buttons stay solid parchment chips on the glass slab — don't let them go
 * translucent too or they'd mush into the map. (Their chrome comes from the
 * .grid-ctrls .zoom-btn / > button rules below, unchanged.) */
.grid-ctrls label {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-family: var(--bb-font-display);
  font-size: 0.75rem;           /* 12px */
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--bb-ink-soft);
}
.grid-ctrls input { width: 60px; min-width: 60px; }
.grid-ctrls > button,
/* #215/#224 Export map ▾ and Map display ▾ each sit in a positioned wrapper (for
 * their dropdowns), so they aren't direct children of .grid-ctrls — mirror the
 * sibling view-control chrome onto them. */
.grid-ctrls .map-display-wrap > button {
  font-family: var(--bb-font-display);
  font-size: 0.8125rem;             /* 13px */
  letter-spacing: 0.14em;
  text-transform: uppercase;
  background: var(--bb-parchment-light);
  color: var(--bb-ink);
  border: 1px solid var(--bb-rule);
  box-shadow: 0 1px 0 var(--bb-inset-light) inset, 0 1px 2px rgba(80,40,10,0.15);
  padding: 6px 12px;
}
.grid-ctrls > button:hover,
.grid-ctrls .map-display-wrap > button:hover {
  background: var(--bb-parchment-mid);
  border-color: var(--bb-ink-faint);
}

/* Map display popover (#224): reuses .bastions-menu card chrome but holds
 * settings-rows, not menu-rows. Right-anchored (it sits near the cluster's right
 * edge) and given the calm two-line row shape the toggles had in Settings. */
.map-display-panel {
  right: 0;
  left: auto;
  padding: 6px 14px;
  min-width: 320px;
}
/* On the floating dock (desktop) Map display opens UPWARD — the dock lives at the
 * canvas bottom-left, so a downward panel would fall off. Unclipped thanks to the
 * overflow:visible .stage restructure (#230 pt1). Phones keep the fixed bottom
 * sheet (the tier rule below), so scope this to wider viewports.
 *
 * The negation is NOT tidiness (#283). This selector is (0,2,0) against the
 * sheet's bare `.map-display-panel` (0,1,0), so on a landscape phone — which
 * clears min-width:701px — it would win, and `bottom: calc(100% + 6px)` on an
 * element the sheet made `position: fixed` resolves against the VIEWPORT: the
 * panel's bottom edge lands 6px above the top of the screen and Map display
 * opens into nothing. `not (…)` rather than the De Morgan pair because it reads
 * as "everything except the phone tier", which is what the next author needs. */
@media (min-width: 701px) and (not ((max-height: 500px) and (pointer: coarse))) {
  .grounds-view-dock .map-display-panel {
    top: auto;
    bottom: calc(100% + 6px);
    left: 0;
    right: auto;
  }
}
.map-display-panel .settings-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  column-gap: 10px;
  row-gap: 2px;
  align-items: center;
  padding: 6px 0;
  border-bottom: none;
}
.map-display-panel .settings-row + .settings-row { border-top: 1px solid var(--bb-rule-soft); }
.map-display-panel .settings-row .settings-sub { grid-column: 1 / -1; }
/* checkbox rows hang the description under the label text, clear of the box */
.map-display-panel .settings-row:has(.view-toggle) .settings-sub { padding-left: 26px; }
.map-display-panel .settings-subhead { margin: 0.6rem 0 0.125rem; }

/* Map display popover on phones (#224): keep the trigger a ≥44px tap target and
 * render the panel as a bottom sheet so its content never overflows the wrapped
 * cluster. The on-grid reading toggles (names, marginalia) stay reachable — they
 * are legit aids for the view-only phone map.
 *
 * Tier query (#283) — re-anchoring a popover to a viewport edge is a tier act.
 * Its complement above MUST carry the negation or the sheet breaks outright: the
 * upward-opening rule is `.grounds-view-dock .map-display-panel` (0,2,0) against
 * this bare `.map-display-panel` (0,1,0), so `bottom: calc(100% + 6px)` would win
 * on an element this block just made `position: fixed` — whose containing block
 * is the viewport, putting the panel's bottom edge 6px above the top of the
 * screen. The landscape block at the end of this sheet caps its width. */
@media (max-width: 700px), (max-height: 500px) and (pointer: coarse) {
  .grid-ctrls .map-display-wrap > button { min-height: 44px; }
  .map-display-panel {
    position: fixed;
    left: 8px; right: 8px; bottom: 8px; top: auto;
    min-width: 0;
    max-height: 70vh;
  }
  .map-display-panel .view-toggle,
  .map-display-panel .type-pick-btn { min-height: 40px; }
  .map-display-panel .type-pick-btn { display: inline-flex; align-items: center; }
}

/* Floorplan zoom (#121 carve-out) — neutral *view* chrome, so it stays in the
   parchment palette. Gilt signals "the view is off its 100% anchor"; never the
   vermillion mode-btn wash, never red (red = compliance alert only, #131). */
.grid-ctrls .zoom-ctrl { display: inline-flex; align-items: stretch; }
.grid-ctrls .zoom-btn {
  font-family: var(--bb-font-display);
  font-size: 0.8125rem;             /* 13px — matches the grid-ctrls buttons */
  line-height: 1;
  background: var(--bb-parchment-light);
  color: var(--bb-ink);
  border: 1px solid var(--bb-rule);
  box-shadow: 0 1px 0 var(--bb-inset-light) inset, 0 1px 2px rgba(80,40,10,0.15);
  padding: 6px 11px;
  cursor: pointer;
}
.grid-ctrls .zoom-btn + .zoom-btn { margin-left: -1px; }  /* shared borders */
.grid-ctrls .zoom-btn:hover {
  background: var(--bb-parchment-mid);
  border-color: var(--bb-ink-faint);
  position: relative;
  z-index: 1;                       /* lift the joined border on hover */
}
.grid-ctrls .zoom-readout {
  font-variant-numeric: lining-nums tabular-nums;
  /* Fixed width so the readout — and the whole dock — never resizes as the value
   * changes width. The display face has no tabular figures, so "1" is narrower
   * than "2"/"0"; ch-sizing (and a too-small em) let 75%↔100% / 150%↔200% jiggle
   * the dock (#230 pt1 follow-up). box-sizing is border-box here, so this width
   * must clear the widest reading ("200%") PLUS the button's own h-padding. */
  min-width: 5em;
  box-sizing: border-box;
  text-align: center;
}
/* Gilt underline only when zoom ≠ 100%. */
.grid-ctrls .zoom-readout.is-zoomed {
  color: var(--bb-gilt-deep);
  box-shadow: 0 1px 0 var(--bb-inset-light) inset, 0 1px 2px rgba(80,40,10,0.15),
              inset 0 -2px 0 var(--bb-gilt);
}
.grid-ctrls .zoom-btn:disabled {
  opacity: 0.38;
  cursor: not-allowed;
}
.grid-ctrls .zoom-btn:disabled:hover {
  background: var(--bb-parchment-light);
  border-color: var(--bb-rule);
}
.grid-ctrls .zoom-icon-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: var(--bb-parchment-light);
  color: var(--bb-ink);
  border: 1px solid var(--bb-rule);
  box-shadow: 0 1px 0 var(--bb-inset-light) inset, 0 1px 2px rgba(80,40,10,0.15);
  padding: 6px 9px;
  cursor: pointer;
}
.grid-ctrls .zoom-icon-btn:hover {
  background: var(--bb-parchment-mid);
  border-color: var(--bb-ink-faint);
}
/* Fullscreen is a persistent toggled state → it earns the stronger gilt cue. */
.grid-ctrls .zoom-icon-btn[aria-pressed="true"] {
  color: var(--bb-gilt-deep);
  border-color: var(--bb-gilt);
}
.grid-ctrls .zoom-glyph:not([hidden]) { display: block; }
.grid-ctrls .zoom-btn:focus-visible,
.grid-ctrls .zoom-icon-btn:focus-visible {
  outline: 2px solid var(--bb-gilt);
  outline-offset: 2px;
}
/* Hairline seam separating the view cluster from the Fit/Save actions. */
.grid-ctrls .grid-ctrls-seam {
  align-self: stretch;
  width: 1px;
  margin: 2px 2px;
  background: var(--bb-rule);
  opacity: 0.6;
}

/* Grid size readout (#129) — a live cols × rows chip in the stage tools.
 * Resizing itself is done by dragging the floor-edge handles (.grid-edge-*
 * below). */
.grid-dims {
  font-family: var(--bb-font-mono);
  font-size: 0.8125rem;             /* 13px */
  color: var(--bb-ink);
  font-variant-numeric: lining-nums tabular-nums;
  padding: 0 6px;
  white-space: nowrap;
  display: inline-flex;
  align-items: baseline;
}
/* Each number reserves room for up to two digits (GRID_MAX = 60) so the
 * 9→10 transition doesn't shove the neighbouring controls sideways. */
.grid-dim-num {
  display: inline-block;
  min-width: 2ch;
  text-align: center;
}
.grid-dim-x {
  padding: 0 1px;
  color: var(--bb-ink-faint);
}

/* Grid edge-drag resize handles (#129) — one capsule bar per grid edge, shown
 * only in idle edit mode. The palette is gilt/ambient (deliberately quieter
 * than the vermillion facility ghost-handle: grid edges are structural canvas
 * chrome, not a hot object selection). `.grid-edge-bar` is the visible capsule;
 * `.grid-edge-hit` is the wider transparent grab/focus/cursor target. */
.grid-edge-bar {
  pointer-events: none;
  fill: var(--bb-parchment-light);
  stroke: var(--bb-rule-soft);
  stroke-width: 1.5;
  transition: transform 0.08s ease-out, fill 0.08s ease-out, stroke 0.08s ease-out;
  transform-box: fill-box;
  transform-origin: center;
}
.grid-edge-hit {
  pointer-events: all;
  fill: transparent;
}
.grid-edge-handle:hover .grid-edge-bar,
.grid-edge-handle:focus-within .grid-edge-bar {
  fill: var(--bb-gilt-fill);
  stroke: var(--bb-gilt);
  transform: scale(1.18);
}
.grid-edge-handle.dragging .grid-edge-bar {
  fill: var(--bb-gilt);
  stroke: var(--bb-gilt-deep);
  transform: scale(1.18);
}
/* Hard-stop feedback: flash gilt-bright while the drag pushes past the bound
 * (or for ~180ms on a blocked keyboard bump — the JS toggles the class). */
.grid-edge-handle.bound-hit .grid-edge-bar {
  fill: var(--bb-gilt-bright);
  stroke: var(--bb-gilt-deep);
}
/* Both directions blocked (at GRID_MAX and flush to content): dim + no-drag. */
.grid-edge-handle.at-bound .grid-edge-bar {
  opacity: 0.4;
}
.grid-edge-handle.at-bound .grid-edge-hit {
  cursor: not-allowed !important;
}
/* Keyboard focus ring on the (otherwise invisible) hit target. */
.grid-edge-hit:focus-visible {
  outline: 2px solid var(--bb-gilt);
  outline-offset: 2px;
}

/* ---- Edit-mode toolbar (Codex §6.3) ------------------------------------- */

/* Command rail (§5.9 item 5): the toolbar (row 1) + the constant-height status
 * zone (row 2). Keeping both rows together as one band is what lets arming a
 * tool swap the status copy in place — zero canvas reflow.
 *
 * #275 moved it OUT of .stage: it is now a full-width band above .grounds-body,
 * inside .grounds-frame. Two consequences, both deliberate:
 *   - It no longer needs `position: sticky` (nothing scrolls under it now — it
 *     sits above the scroller rather than inside it), nor the `margin-inline:
 *     -22px` that used to cancel .stage's padding. It carries plain 22px
 *     h-padding instead, which keeps the toolbar's left edge aligned with the
 *     canvas below.
 *   - Its width is `viewport − 44` instead of `viewport − 366`, which is the
 *     entire fix: nine mode buttons need 925–992px depending on display face,
 *     so one row used to require a ~1358px window and now requires 1036px.
 * The translucent gradient stays: it is the band's material, and it still reads
 * correctly against the parchment page even with nothing scrolling beneath. */
.grounds-command {
  position: static;
  /* Vestigial since #275 and kept only as a floor: the rail is a normal-flow
   * block above .grounds-body now, so it cannot spatially overlap the canvas or
   * the dock the way the sticky version could. It still orders correctly as a
   * flex item if a future absolutely-positioned child needs a stacking context
   * here — but nothing currently depends on it, and the dock's 30 is about
   * clearing the canvas, not this. */
  z-index: 20;
  padding: 10px 22px 8px;
  background: linear-gradient(180deg,
      color-mix(in srgb, var(--bb-parchment)     92%, transparent),
      color-mix(in srgb, var(--bb-parchment-mid) 85%, transparent));
  backdrop-filter: blur(6px);
  /* Soft separator, not the heavy --bb-rule — the canvas sits below with a
   * 16px gap (.stage-body padding-top) so its own matted top edge shows (#160). */
  border-bottom: 1px solid var(--bb-rule-soft);
}

/* The toolbar wraps BY FAMILY (#275). Row count is a pure function of the
 * available width — no breakpoint — so the rail's height is constant at any
 * given width, arming a tool still reflows nothing, and it behaves correctly in
 * fullscreen (where the frame is wider than a media query would assume).
 * `column-gap` is the family separator and applies only BETWEEN .tool-groups;
 * the -1px joined-plaque margin below still runs inside them. */
.edit-mode-toolbar {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  column-gap: 18px;                /* family separator — replaces the hairline */
  row-gap: 6px;
  margin-bottom: 8px;              /* small gap before the status zone (row 2) */
}

/* A family: the unbreakable unit. gap:0 inside so the plaque stays joined, and
 * because a group is one flex item the row can only ever break between groups.
 * Remove these wrappers and the toolbar's column-gap would apply between all
 * nine buttons, dissolving the joined plaque entirely — they are structural. */
.edit-mode-toolbar .tool-group {
  display: flex;
  align-items: center;
}

/* Joined plaque (§6.3): adjacent mode buttons share their inner border via a
 * -1px left margin. Scoped inside a group, which is where buttons are ever
 * siblings — so this survives the wrap untouched. */
.edit-mode-toolbar .mode-btn + .mode-btn { margin-left: -1px; }

/* `.tool-seam` (#139) is RETIRED from the toolbar (#275) — families separate by
 * air now. A hairline is the one separator form that cannot be made correct
 * across a wrap: a seam element landing at a row edge draws a dangling divider,
 * and there is no CSS to suppress it. The plaque's own borders already carry the
 * vertical-hairline vocabulary, and the idiom survives on the view-dock
 * (.grid-ctrls-seam), whose row never wraps. */

/* The toolbar's "show names on floorplan" `.view-toggle` and its custom
 * checkbox chrome are DELETED (#275): the toggle moved into the Map display ▾
 * panel in #224 and the markup has carried no `.view-toggle` since. The live
 * copies are `#map-display-menu .view-toggle` (below) and the Settings dialog's. */

/* Sourcebooks disclosure (Charter, #170) — closed by default so the cover
   page stays light; the summary line carries the at-a-glance scope. */
.charter-sources {
  margin: 22px 0 0;
  border-top: 1px solid var(--bb-rule);
}
/* THE CALENDAR BLOCK (#310) shares Sourcebooks' anatomy exactly — same rule,
   same summary row, same uppercase title and italic right-set summary. That is
   the point: it is the colophon's second per-bastion configuration block, and a
   second visual language for it would make two settings look like two kinds of
   thing. Its own selectors below carry only what differs: the founding and pace
   rows, which are a statement plus a quiet verb rather than a toggle. */
.charter-calendar {
  margin: 0;
  border-top: 1px solid var(--bb-rule);
}
.charter-calendar > summary {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 10px 0;
  cursor: pointer;
  list-style: none;
}
.charter-calendar > summary::-webkit-details-marker { display: none; }
.charter-cal-row {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 0.5rem;
}
/* The calendar picker is a PARCHMENT DROPDOWN, not a native <select>: this
   control sits inches from the Charter's own Add-credential menu, which is that
   pattern, and an OS-rendered popup beside it reads as another application.
   (The house rule is stated in index.html twice: native belongs inside a dialog
   form; in-page chrome uses createDropdown.)

   It reuses .floor-select's INK — a stripped button with a hand-inked caret —
   but overrides its LAYOUT, which is built to fill the floor spine's column.
   Here it sits inline beside its label in a settings row, and it speaks the
   colophon's body voice rather than the canvas chrome's display voice. */
.charter-cal-wrap { flex: 0 0 auto; }
.charter-cal-wrap .floor-select {
  display: inline-block;
  width: auto;
  min-width: 15rem;
  font-family: var(--bb-font-text);
  font-size: 0.9375rem;
}
.charter-cal-pick-label { flex: 0 0 auto; }
/* 340px is the Bastions menu's measure, sized for a bastion name plus its
   meta line. A calendar name is one short phrase, so the menu narrows to the
   trigger it drops from instead of hanging off the side of it. */
.charter-cal-menu { min-width: 15rem; }
.charter-sources > summary {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 10px 0;
  cursor: pointer;
  list-style: none;
}
.charter-sources > summary::-webkit-details-marker { display: none; }
/* Both colophon disclosures sat at 37-39px tall — under the 44px floor a coarse
   pointer needs, shipped and unnoticed because a fine pointer never feels it.
   Placed after BOTH base rules so source order can never work against it. */
@media (pointer: coarse) {
  .charter-calendar > summary,
  .charter-sources > summary { min-height: 44px; }
}
.charter-sources-title {
  font-family: var(--bb-font-display);
  font-size: 0.78rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--bb-ink-soft);
}
.charter-sources-summary {
  margin-left: auto;
  font-family: var(--bb-font-italic);
  font-style: italic;
  font-size: 0.8125rem;
  color: var(--bb-ink-soft);
}
.charter-sources > summary::after {
  content: "";
  flex: none;
  width: 7px; height: 7px;
  margin-right: 3px;
  border-right: 1.5px solid var(--bb-ink-soft);
  border-bottom: 1.5px solid var(--bb-ink-soft);
  transform: rotate(45deg);
  transition: transform 120ms;
}
.charter-sources[open] > summary::after { transform: rotate(-135deg); }
.charter-sources > summary:hover .charter-sources-title,
.charter-sources > summary:focus-visible .charter-sources-title { color: var(--bb-vermillion); }
.charter-sources > summary:hover::after,
.charter-sources > summary:focus-visible::after { border-color: var(--bb-vermillion); }
.charter-sources > summary:focus-visible {
  outline: 2px solid var(--bb-vermillion);
  outline-offset: 2px;
}
.charter-sources[open] { padding-bottom: 8px; }
.charter-sources .chapter-sub { margin: 0 0 8px; }

/* Settings dialog checkboxes — same visual as toolbar toggle but readable size.
   Shared with the per-bastion Sourcebooks control on the Charter (#170) and the
   Grounds Map display popover (#224). */
.settings-body .view-toggle,
.charter-sources .view-toggle,
#welcome-dialog .view-toggle,
#map-display-menu .view-toggle {
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: 8px;
  font-size: 0.875rem;
  color: var(--bb-ink);
  cursor: pointer;
  user-select: none;
}
.settings-body .view-toggle input,
.charter-sources .view-toggle input,
#welcome-dialog .view-toggle input,
#map-display-menu .view-toggle input {
  position: absolute; width: 1px; height: 1px;
  margin: -1px; padding: 0; overflow: hidden;
  clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}
.settings-body .view-toggle::before,
.charter-sources .view-toggle::before,
#welcome-dialog .view-toggle::before,
#map-display-menu .view-toggle::before {
  content: ''; display: inline-block;
  width: 18px; height: 18px; flex: 0 0 18px;
  border: 1px solid var(--bb-rule);
  background: var(--bb-parchment-light);
  border-radius: 0; box-sizing: border-box;
}
.settings-body .view-toggle:has(input:checked)::before,
.charter-sources .view-toggle:has(input:checked)::before,
#welcome-dialog .view-toggle:has(input:checked)::before,
#map-display-menu .view-toggle:has(input:checked)::before {
  background: var(--bb-vermillion-deep);
  border-color: var(--bb-vermillion-deep);
}
.settings-body .view-toggle::after,
.charter-sources .view-toggle::after,
#welcome-dialog .view-toggle::after,
#map-display-menu .view-toggle::after {
  content: ''; position: absolute;
  left: 5px; top: 50%;
  width: 5px; height: 10px;
  border-right: 2px solid var(--bb-parchment-light);
  border-bottom: 2px solid var(--bb-parchment-light);
  transform: translateY(-65%) rotate(45deg);
  opacity: 0; pointer-events: none;
}
.settings-body .view-toggle:has(input:checked)::after,
.charter-sources .view-toggle:has(input:checked)::after,
#welcome-dialog .view-toggle:has(input:checked)::after,
#map-display-menu .view-toggle:has(input:checked)::after { opacity: 1; }
.settings-body .view-toggle:has(input:focus-visible)::before,
.charter-sources .view-toggle:has(input:focus-visible)::before,
#welcome-dialog .view-toggle:has(input:focus-visible)::before,
#map-display-menu .view-toggle:has(input:focus-visible)::before {
  box-shadow: 0 0 0 2px var(--bb-vermillion);
}

/* Locked row (Core — always on) */
.settings-row.locked .view-toggle {
  cursor: default; color: var(--bb-ink-faint);
}
.settings-row.locked .view-toggle::before {
  background: var(--bb-vermillion-deep); border-color: var(--bb-vermillion-deep);
  opacity: 0.45;
}
.settings-row.locked .view-toggle::after { opacity: 1; }

/* Row layout */
.settings-row {
  display: flex; align-items: center; gap: 10px;
  padding: 8px 0;
  border-bottom: 1px solid var(--bb-rule-soft);
}
.settings-group .settings-row:last-child { border-bottom: none; }

/* #216 — inside the Settings dialog, rows grid into a calm two-line shape:
 * label (+ trailing control) on the first line, the italic description on its
 * own full-width line below, so every description starts at the same edge
 * instead of trailing labels of assorted lengths. Per-row dividers drop — the
 * group-title underline alone bounds each section. Scoped to the dialog: the
 * founding form reuses .settings-row and keeps the flex recipe. */
#settings-dialog .settings-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  column-gap: 10px;
  row-gap: 2px;
  align-items: center;
  border-bottom: none;
}
#settings-dialog .settings-row .settings-sub { grid-column: 1 / -1; }
/* checkbox rows hang the description under the label text, clear of the box */
#settings-dialog .settings-row:has(.view-toggle) .settings-sub { padding-left: 26px; }
/* #218 — a row whose only text IS its description (the Build stamp) keeps that
   text in column 1 beside the trailing control, instead of spanning the full
   width and leaving an empty cell where a label would have been. */
#settings-dialog .settings-row:not(:has(.settings-row-label)) .settings-sub {
  grid-column: 1;
}
/* trailing controls sit beside the label, never after the description */
#settings-dialog .settings-row .settings-action,
#settings-dialog .settings-row .version-badge {
  grid-row: 1;
  grid-column: 2;
}
/* #284 — the Companion service sentence carries a relay origin, and a URL has
   no break opportunities of its own. Under a minmax(0, 1fr) track it would
   otherwise force the grid wider than the dialog on a phone. The URL stays out
   of .settings-row-label, which is white-space: nowrap. */
#settings-companion-row .settings-sub { overflow-wrap: anywhere; }

/* The widen to 760px gives .settings-sub and the group intros a ~130ch measure,
 * which is roughly twice a readable line and leaves one-word orphans on the
 * longer ones. Cap the prose, not the rows: the trailing controls stay hard
 * right, so nothing that was aligned stops being aligned. (The card subs sit in
 * naturally narrow columns and need no cap; the two-column toggle block caps
 * its own four cells by construction.) */
#settings-dialog .settings-row .settings-sub { max-width: 62ch; }
/* A group intro is one short line, not body prose, so it gets a little more
 * room than a row's description — enough that the four of them stay one line
 * each and read as siblings. */
#settings-dialog .settings-group-sub { max-width: 74ch; }

/* CONDITIONAL, and that is the whole point (#216 removed per-row dividers). In
 * the connected state three sync rows land immediately above this readout and
 * "Using 2.4 MB" pressed against them reads as the *cloud's* figure — so the
 * rule appears exactly then, keyed off the rows being non-empty. In every other
 * state the group carries no intra-group line at all, which is what keeps this
 * separation rather than decoration: a line that is always there is a divider. */
#settings-cloud-rows:not(:empty) + #settings-storage-row {
  margin-top: 4px;
  padding-top: 9px;
  border-top: 1px solid var(--bb-rule-soft);
}

/* #216 — toggle-only groups (Map & floorplan / Handout / At the table) sit
 * tighter so minor tweaks read as minor next to the roomy tile/action groups. */
#settings-dialog .settings-group--compact .settings-row { padding: 6px 0; }

/* #216 — on touch, the compact density must not shrink tap targets: hold every
 * settings control (checkbox labels, action buttons, N/E/S/W picks) at 40px.
 * Carries the tier's height branch (#283): a tap target is a phone fact, and a
 * phone held sideways still has thumbs. (Strictly this wants `pointer: coarse`
 * alone, but that would change tablets too — out of scope here.) */
@media (max-width: 700px), (max-height: 500px) and (pointer: coarse) {
  .settings-body .view-toggle,
  .settings-body .settings-action,
  .settings-body .type-pick-btn {
    min-height: 40px;
  }
  /* labels don't centre their text in a taller box on their own */
  .settings-body .type-pick-btn {
    display: inline-flex;
    align-items: center;
  }
}

.settings-sub {
  font-family: var(--bb-font-italic); font-style: italic;
  font-size: 0.8125rem; color: var(--bb-ink-soft);
  flex: 1;
}
.settings-count {
  font-size: 0.75rem; color: var(--bb-ink-soft);
  font-variant-numeric: lining-nums tabular-nums;
  white-space: nowrap;
}

/* #150 backup/restore action rows — a plain bold label + a quiet ghost button.
   The button stays text-only (no ↥/↧ arrows) so the whole-state actions read
   apart from the per-bastion Import/Export glyphs in the Bastions menu. */
.settings-row-label {
  font-family: var(--bb-font-display);
  font-size: 0.875rem; color: var(--bb-ink);
  white-space: nowrap;
}
.settings-action {
  margin-left: auto; flex: none;
  background: var(--bb-parchment-light);
  border: 1px solid var(--bb-rule); border-radius: 0;
  color: var(--bb-ink);
  padding: 5px 14px;
  font-family: var(--bb-font-display);
  font-size: 0.8125rem; letter-spacing: 0.06em;
  cursor: pointer; white-space: nowrap;
  min-width: 7rem; text-align: center; /* #216 — equal plates, aligned edges */
}
.settings-action:hover {
  background: var(--bb-accent-soft);
  border-color: var(--bb-vermillion);
  color: var(--bb-vermillion);
}
.settings-body {
  margin: 14px 0 0;
  display: flex;
  flex-direction: column;
  gap: 16px;
}

/* #136 — the Settings dialog kept growing past the viewport, pushing Done
 * off-screen. Cap the form to the viewport and let the settings list scroll
 * inside, so the Done button stays pinned and always reachable. When the
 * content is short the dialog still shrinks to fit (no forced tall box).
 * #216 — the dialog itself must never scroll (the UA gives <dialog> its own
 * overflow, which doubled the scrollbar on phones); dvh keeps the cap honest
 * under mobile URL bars, with vh as the older-engine fallback. */
#settings-dialog {
  overflow: hidden;
}
#settings-dialog .dialog-form {
  max-height: 88vh;
  max-height: 88dvh;
}
/* .dialog-sub's 1rem bottom margin stacks on the form's 14px flex gap (no
 * collapsing in flex) — twice the 16px group rhythm. The gap alone spaces it. */
#settings-dialog .dialog-sub { margin-bottom: 0; }
#settings-dialog .settings-body {
  margin-top: 0;       /* the form's own gap spaces it under the intro line */
  min-height: 0;       /* allow the flex child to shrink below its content … */
  overflow-y: auto;    /* … and scroll, rather than pushing Done off-screen */
  padding-right: 12px; /* house gutter so the scrollbar doesn't crowd toggles */
  scrollbar-gutter: stable;
}
#settings-dialog .dialog-actions {
  flex-shrink: 0;      /* the Done row never compresses */
}

/* Settings groups (Content / Handout / …) */
.settings-group {
  display: flex;
  flex-direction: column;
}
.settings-group-title {
  margin: 0;
  font-family: var(--bb-font-display);
  font-size: 0.75rem;
  font-weight: 600;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--bb-gilt-deep);
  padding-bottom: 4px;
  border-bottom: 1px solid var(--bb-rule-soft);
}
.settings-group-sub {
  margin: 4px 0 2px;
  font-family: var(--bb-font-italic);
  font-style: italic;
  font-size: 0.8125rem;
  color: var(--bb-ink-soft);
}
/* #216 — sub-cluster label inside a group ("On exports", "Palette"): a
 * lighter echo of the group title, so clusters read as units without a rule. */
.settings-subhead {
  margin: 0.75rem 0 0.125rem;
  font-family: var(--bb-font-display);
  font-size: 0.6875rem;
  font-weight: 600;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--bb-ink-soft);
}

/* Theme picker tiles — 2-column grid so 4 tiles sit as 2×2. Inside the (now
 * 760px) Settings dialog they run 4 abreast on one line instead; see the
 * #settings-dialog override below. #vtt-export-dialog keeps the 2-col default,
 * and its own single-column carve-out, at .modal's 520px. */
.theme-option-group {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 10px;
  padding: 10px 0 4px;
}
/* One row of four. Not `auto-fit, minmax(150px, …)`: four tiles reflowing to
 * three leaves a lone orphan on the second line, which is worse than the 2×2
 * it replaced. The width branch below takes it back to 2×2 wholesale.
 * `minmax(0, 1fr)` + .theme-tile's own `min-width: 0` removes the min-content
 * floor, so a long label OVERFLOWS SILENTLY into the gutter rather than
 * widening its track — "Cartographer's" in Cinzel is the tight one, measured at
 * ~136px against a ~145px box. Re-measure `.theme-tile-label` scrollWidth vs
 * clientWidth in Cartographer before changing this grid, the type scale, or
 * the dialog width. */
#settings-dialog .theme-option-group {
  grid-template-columns: repeat(4, minmax(0, 1fr));
}
.theme-tile {
  /* #218 — contain the .sr-only radio inside the label. Without a positioned
   * ancestor here, the absolutely-positioned radio's containing block is the
   * <dialog> (UA position:absolute); focusing it on click makes the browser
   * scroll the whole dialog into view, shooting it off-screen (blank Settings). */
  position: relative;
  display: flex;
  flex-direction: column;
  gap: 6px;
  padding: 10px;
  border: 1px solid var(--bb-rule);
  border-radius: 4px;
  cursor: pointer;
  min-width: 0;
  background: var(--bb-parchment-light);
  transition: border-color 0.15s, box-shadow 0.15s;
}
.theme-tile:hover {
  border-color: var(--bb-vermillion);
}
.theme-tile--selected {
  border-color: var(--bb-vermillion-deep);
  box-shadow: 0 0 0 1px var(--bb-vermillion-deep);
}
.theme-swatches {
  display: flex;
  gap: 4px;
}
.theme-swatch {
  display: block;
  width: 22px;
  height: 22px;
  border-radius: 50%;
  border: 1px solid var(--bb-rule);
}
.theme-tile-body {
  display: flex;
  flex-direction: column;
  gap: 2px;
}
.theme-tile-label {
  font-family: var(--bb-font-display);
  font-size: 0.8125rem;
  font-weight: 600;
  letter-spacing: 0.05em;
  color: var(--bb-ink);
}
.theme-tile-desc {
  font-family: var(--bb-font-italic);
  font-style: italic;
  font-size: 0.75rem;
  color: var(--bb-ink-soft);
}
.sr-only {
  position: absolute;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip: rect(0,0,0,0);
  white-space: nowrap;
  border-width: 0;
}

/* ---- Settings errand cards (Your library) ------------------------------- */
/* Shape as the third big-vs-minor signal, beside #216's density. The library's
 * group used to be five rows identical to every preference row in the dialog,
 * which left the two whole-library features — cloud sync and homebrew — reading
 * as small print beside a theme picker that was the only thing in Settings with
 * a form. A card hides nothing, so #216's "density, never tabs or <details>"
 * clause is untouched: this is not disclosure, it is weight.
 *
 * NO PANEL BEHIND THE CARDS. A recessed field was drawn and cut: the dialog's
 * own background is a parchment-light → parchment-mid gradient, so a
 * --bb-parchment fill lands invisible against its local backdrop in Codex and
 * renders as a distinct sunken well in Arcane — one declaration, two forms, by
 * theme. The group title's underline already bounds the group and the cards
 * carry their own borders. */
.settings-cards {
  display: grid;
  /* Three across at the dialog's ~700px of content — 223px each, measured.
     Not `auto-fit`: three cards reflowing to two leave a lone orphan, so the
     column count steps explicitly (3 → 2-with-the-last-spanning → 1) in the
     width branches below. The earlier shape here was 2 abreast with Back up &
     restore spanning the foot; three across is 56px shorter in Codex, 75px in
     Cartographer, and identical between them, which the split shape was not. */
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 10px;
  padding: 6px 0 2px;
}
/* The cloud card's mount is a pass-through: cloudsync.js owns the card, the
   grid owns the placement, and an empty mount occupies no cell. */
#settings-cloud-card { display: contents; }

.settings-card {
  display: flex;
  flex-direction: column;
  gap: 5px;
  min-width: 0;
  padding: 10px 12px 11px;
  border: 1px solid var(--bb-rule);
  border-radius: 4px;              /* the .theme-tile radius — same family */
  background: var(--bb-parchment-light);
}
.settings-card-title {
  margin: 0;
  font-family: var(--bb-font-display);
  /* .settings-row-label's size, deliberately. The card supplies the emphasis;
     a step up would make this h4 outrank its own h3 group title (0.75rem) and
     add a type step the scale doesn't have. */
  font-size: 0.875rem;
  font-weight: 600;
  letter-spacing: 0.03em;
  color: var(--bb-ink);
}
.settings-card-sub {
  margin: 0;
  font-family: var(--bb-font-italic);
  font-style: italic;
  font-size: 0.8125rem;
  color: var(--bb-ink-soft);
}
/* The verb rail bottoms out, so the three cards' buttons align across the row
   however long their state lines run. That alignment is the whole reason the
   group reads as composed rather than stacked. */
.settings-card-rail {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin-top: auto;
  padding-top: 7px;
}
/* .settings-action's 7rem floor is sized for a right-anchored row, not for two
   buttons inside a card — at the display face's width in Cartographer the pair
   would not fit a half-width card. They size to their labels here. */
.settings-card .settings-action { min-width: 0; margin-left: 0; }

/* A card stating a fact rather than offering an errand (sync with no companion
   service). Quieter border, no rail — #284's "shown, never warned", now in the
   idiom of its neighbours rather than as the one row among cards. */
.settings-card--fact {
  border-color: var(--bb-rule-soft);
  background: none;
}

/* ---- Settings two-column toggle block ----------------------------------- */
.settings-cols {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  /* .settings-group--compact's rows carry no horizontal padding, so the gap is
     the only thing between the two columns' text. */
  column-gap: 20px;
}
/* Side-by-side rows stretch to the taller of the pair, and .settings-row's own
   `align-items: center` then spreads label and description apart inside the
   surplus — a one-line description floats away from the toggle it belongs to.
   Pack both to the top of the cell instead. */
.settings-cols .settings-row { align-content: start; }

/* WIDTH-ONLY, never the compound phone tier (#283). The tier's height branch
 * fires on a landscape phone (~800×440) — collapsing a two-column block there
 * would make the dialog TALLER on the one viewport with no height to spare.
 * The tap-target block above is a different question and correctly keeps the
 * compound query: a phone held sideways still has thumbs. */
@media (max-width: 700px) {
  #settings-dialog .theme-option-group { grid-template-columns: repeat(2, 1fr); }
  .settings-cols { grid-template-columns: 1fr; }
}
/* The card grid steps a branch earlier than the rest: below ~820px the dialog
   is 90vw, and a third of that leaves the Back up card's two verbs no room on
   one line. Two columns with the LAST card spanning, so there is never an
   orphan half-card — and it is a plain wider card, not a second layout. */
@media (max-width: 820px) {
  .settings-cards { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .settings-cards > .settings-card:last-child { grid-column: 1 / -1; }
}
@media (max-width: 560px) {
  .settings-cards { grid-template-columns: 1fr; }
}

/* Mode buttons (§6.3) — joined plaque with a gilt active fill. The armed tool
 * is a current/active state, not an alert, so it wears gold, not red (#131 —
 * red is reserved for danger/Cancel, which sits right below in this rail). The
 * fill mirrors the status-chip recipe (gilt-deep + parchment-light, ~5.98:1)
 * so the pressed button and the banner's tool chip read as one gold cluster. */
.mode-btn {
  padding: 7px 14px;
  font-family: var(--bb-font-display);
  font-size: 0.75rem;            /* 12px */
  letter-spacing: 0.12em;
  text-transform: uppercase;
  background: var(--bb-parchment-light);
  color: var(--bb-ink-soft);
  border: 1px solid var(--bb-rule);
  border-radius: 0;
  box-shadow: none;
}
/* Hover is colourless (parchment-mid) so gilt reads unambiguously as "armed" —
 * no red wash to pre-echo the old active colour, no gold to pre-echo the new. */
.mode-btn:hover {
  background: var(--bb-parchment-mid);
  color: var(--bb-ink);
  border-color: var(--bb-rule);
}
.mode-btn.active,
.mode-btn[aria-pressed="true"] {
  background: var(--bb-gilt-deep);
  color: var(--bb-parchment-light);
  border-color: var(--bb-gilt-deep);
  text-shadow: 0 1px 0 rgba(0,0,0,0.25);
  box-shadow: 0 1px 0 var(--bb-inset-faint) inset;
  /* Lift active button so its borders overlap neighbours cleanly. */
  position: relative;
  z-index: 1;
}
/* No active:hover rule: `.mode-btn[aria-pressed="true"]` (b:2) ties `.mode-btn
 * :hover` (b:2) on specificity and wins on source order, so the gilt fill holds
 * on hover without a rule that would fight the per-theme dark-text overrides. */
.mode-btn:focus-visible {
  outline: 2px solid var(--bb-vermillion);
  outline-offset: 2px;
  z-index: 2;
}

/* ---- Register of Chambers (#134) ---------------------------------------- */

/* The Register is a peer chapter (#121): its own <section hidden> that the
 * chapter switch shows/hides. No floorplan-chrome hiding is needed here —
 * Plates lives in a separate section that's hidden whole when another chapter
 * is active. */

.register-view {
  /* Lining/tabular figures across the whole view (gold, days, levels). */
  font-variant-numeric: lining-nums tabular-nums;
  /* No own padding — the .chapter-register band (18px 22px 40px) governs, so the
   * header sits at the same top offset as the other chapters (nit 1 follow-up). */
}
/* ---- Register card management (#121 Stage B) ---------------------------- */
/* On the leaf the action row sits directly beneath the head, whose own bottom
   border already rules it off — a second hairline 10px below the first was the
   "double divider" a basic room showed, since it has no content between them.
   The rule is kept only where .reg-actions still follows content. */
.reg-actions {
  display: flex;
  flex-direction: column;
  gap: 8px;
  margin-top: 12px;
  padding-top: 10px;
  border-top: 1px solid var(--bb-rule-soft);
}
.reg-leaf-head + .reg-actions {
  margin-top: 0;
  padding-top: 0;
  border-top: 0;
}
.reg-act-row {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
}
.reg-act {
  background: var(--bb-parchment-light);
  border: 1px solid var(--bb-rule-soft);
  border-radius: 0;
  box-shadow: none;
  padding: 5px 11px;
  font-family: var(--bb-font-display);
  font-size: 0.78rem;
  letter-spacing: 0.03em;
  color: var(--bb-ink);
  cursor: pointer;
}
.reg-act:hover { background: var(--bb-parchment-mid); border-color: var(--bb-rule); }
.reg-act:focus-visible { outline: 2px solid var(--bb-vermillion); outline-offset: 2px; }
.reg-act:disabled { opacity: 0.5; cursor: not-allowed; }
.reg-act.danger { color: var(--bb-danger); }
.reg-act.danger:hover:not(:disabled) {
  background: var(--bb-danger);
  border-color: var(--bb-danger);
  color: var(--bb-parchment-light);
}
.reg-empty {
  color: var(--bb-ink-faint);
  font-style: italic;
  font-family: var(--bb-font-italic);
}
/* ---- Register codex: the index (#300) ----------------------------------- */
/* The codex (#300). Two panes: a thumb index of every chamber, and the leaf of
 * the one being read. There is no expand/collapse anywhere in this chapter —
 * the accordion that used to span a card full-width, break its grid row and
 * strand its neighbour has no row left to break.
 *
 * `auto-fit` + `minmax(min(100%, …))` rather than a fixed two-column track:
 * the leaf folds under the index intrinsically when the chapter gets narrow, so
 * this chapter adds NO new breakpoint (validate_styles.py makes a new one an
 * argument, and rightly). The index keeps a fixed basis because a rail that
 * reflows its own width as you read is disorienting. */
.reg-codex {
  display: grid;
  grid-template-columns: 268px minmax(0, 1fr);
  gap: 0 28px;
  align-items: start;
}
.reg-index {
  display: flex;
  flex-direction: column;
  gap: 8px;
  position: sticky;
  top: 8px;
  padding-right: 12px;                 /* scroll gutter */
  border-right: 1px solid var(--bb-rule-soft);
  max-height: calc(100vh - 120px);
  overflow-y: auto;
  scrollbar-gutter: stable;
}
.reg-index-find {
  width: 100%;
  min-width: 0;                        /* beats the global 120px input floor */
  background: var(--bb-parchment-light);
  border: 1px solid var(--bb-rule-soft);
  border-radius: 0;
  box-shadow: none;
  padding: 5px 9px;
  font-family: var(--bb-font-text);
  font-size: 0.86rem;
  color: var(--bb-ink);
}
.reg-index-find:focus {
  outline: none;
  border-color: var(--bb-vermillion);
}
/* The grouping switch reuses .type-pick (Openings, defenses) rather than
   growing a third segmented idiom. The caption is two letters because three
   segments measure 185.5px on the cartographer theme's Cinzel against this
   rail's 245px content box: "By" + segments = 210px and fits every face,
   "Group by" + segments = 260px and overflows that one. Measured, not chosen. */
.reg-index-arrange {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-bottom: 6px;
}
.reg-arrange-cap {
  flex: 0 0 auto;
  font-family: var(--bb-font-display);
  font-size: 0.66rem;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--bb-ink-faint);
}
/* Scoped size trim on the shared plaque. Two classes (0,2,0) beats
   .type-pick-btn's (0,1,0), so this sitting far ABOVE the base rule in source
   order is harmless — the same pattern the VTT dialog's overrides use. */
.reg-index .type-pick-btn { padding: 4px 9px; font-size: 0.78rem; }
.reg-index-body {
  display: flex;
  flex-direction: column;
}
.reg-index-group {
  margin: 12px 0 3px;
  font-family: var(--bb-font-display);
  font-size: 0.66rem;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--bb-ink-faint);
  border-bottom: 1px solid var(--bb-rule-soft);
  padding-bottom: 2px;
}
.reg-index-group:first-child { margin-top: 0; }
/* One ruled line per chamber. 30px tall on a fine pointer, and the coarse-
   pointer rule below takes it to the 44px floor — which is the measurement the
   index threshold was derived from. */
.reg-index-row {
  display: flex;
  align-items: center;
  gap: 7px;
  width: 100%;
  text-align: left;
  padding: 4px 6px;
  background: transparent;
  border: 0;
  border-left: 2px solid transparent;
  box-shadow: none;
  cursor: pointer;
  color: var(--bb-ink-soft);
  font: inherit;
}
.reg-index-row:hover { background: var(--bb-gilt-fill); color: var(--bb-ink); }
.reg-index-row:focus-visible {
  outline: 2px solid var(--bb-gilt);
  outline-offset: -2px;
}
.reg-index-row.is-current {
  background: var(--bb-parchment-mid);
  border-left-color: var(--bb-vermillion);
  color: var(--bb-ink);
}
/* The name does NOT grow: the ordinal is part of what the chamber is called,
   so it has to sit against the name rather than drift to the far margin where
   it reads as a separate column of numbers. The state mark takes the slack. */
.reg-index-name {
  flex: 0 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-family: var(--bb-font-display);
  font-size: 0.88rem;
}
.reg-index-row.is-current .reg-index-name { color: var(--bb-ink); }
.reg-index-ord {
  flex: 0 0 auto;
  font-family: var(--bb-font-display);
  font-size: 0.72rem;
  color: var(--bb-vermillion);
}
/* The three state marks are differently SHAPED, not merely differently
   coloured (§4) — and each row also carries the state word in its aria-label,
   so the glyph is never the only channel. */
.reg-index-mark {
  flex: 0 0 auto;
  margin-left: auto;
  padding-left: 6px;
  font-size: 0.72rem;
  line-height: 1;
}
.reg-index-row.is-building .reg-index-mark { color: var(--bb-gilt-deep); }
.reg-index-row.is-condition .reg-index-mark { color: var(--bb-vermillion); }
.reg-index-row.is-haunted .reg-index-mark { color: var(--bb-ink-faint); }
.reg-index-empty {
  margin: 8px 0;
  font-family: var(--bb-font-italic);
  font-size: 0.86rem;
  color: var(--bb-ink-faint);
}
.reg-index-foot {
  margin-top: 10px;
  padding-top: 8px;
  border-top: 1px solid var(--bb-rule-soft);
}

/* ---- Register codex: the leaf (#300) ------------------------------------ */
.reg-leaf-host { min-width: 0; }
.reg-leaf { min-width: 0; }
/* No bottom rule. The head and the action row beneath it are one masthead —
   ruling between them made the verbs read as the first line of the text block,
   which is why they felt cramped against content 6px below while the only
   horizontal rule sat above them. The seam belongs under the verbs, and it is
   drawn by .reg-leaf-body instead (see there for why the body owns it). */
.reg-leaf-head {
  display: flex;
  align-items: flex-start;
  gap: 16px;
  padding-bottom: 10px;
}
.reg-leaf-headtext { flex: 1 1 auto; min-width: 0; }
/* The versal. 74px against the card era's 26px: the glyph ladder says bare ink
   holds full detail well below this, so the size is not about legibility — it
   is that this is the one chapter where the heraldry is the subject. */
.reg-leaf .reg-glyph {
  flex: 0 0 auto;
  width: 74px;
  height: 74px;
}
.reg-leaf .reg-glyph .glyph-svg { width: 74px; height: 74px; }
.reg-leaf-title {
  margin: 0;
  font-family: var(--bb-font-display);
  font-size: 1.85rem;
  line-height: 1.1;
  color: var(--bb-ink);
}
.reg-leaf-title:focus { outline: none; }
.reg-leaf-title:focus-visible {
  outline: 2px solid var(--bb-gilt);
  outline-offset: 3px;
}
/* The name, edited in place under an illuminated initial — the same three-part
   anatomy as the Charter masthead and the Household roster (wrap / aria-hidden
   display carrying the ::first-letter versal / the real input laid over it,
   glass until focused). The card era's labelled NAME box among the buttons was
   the only place in the app that named a thing anywhere but on the thing. */
.reg-leaf-name-wrap {
  position: relative;
  min-height: 40px;
}
.reg-leaf-name-display {
  min-height: 34px;
  padding: 1px 2px 0;
  overflow-wrap: anywhere;
}
.reg-leaf-name-display::first-letter {
  float: left;
  margin-right: 5px;
  font-family: var(--bb-font-display);
  font-size: 46px;
  line-height: 36px;
  color: var(--bb-vermillion);
  /* Always a capital, like a scribe's illuminated initial: a lowercase glyph
     sits at x-height inside the tall floated box and reads as having dropped to
     the second line. Same reasoning as .house-name-display::first-letter. */
  text-transform: uppercase;
}
.reg-leaf-name {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  min-width: 0;                 /* beats the global 120px input floor */
  padding: 1px 2px 0;
  font-family: var(--bb-font-display);
  font-size: 1.85rem;
  line-height: 1.1;
  color: var(--bb-ink);
  caret-color: var(--bb-vermillion);
  background: transparent;
  border: 1px solid transparent;
  border-radius: 0;
  box-shadow: none;
}
/* Named: the overlay reads and the input goes glass until it takes focus. */
.reg-leaf-name-wrap.is-named .reg-leaf-name { color: transparent; caret-color: transparent; }
.reg-leaf-name-wrap.is-named .reg-leaf-name:focus { color: var(--bb-ink); caret-color: var(--bb-vermillion); }
.reg-leaf-name:focus {
  outline: none;
  background: var(--bb-parchment-light);
  border-color: var(--bb-vermillion);
  box-shadow: 0 0 0 2px rgba(168, 40, 29, 0.2);
}
.reg-leaf-name-wrap:focus-within .reg-leaf-name-display { visibility: hidden; }
/* The ordinal is a rubric, not a badge — the chamber's name genuinely includes
   it, so it is set in the title's own line at the title's own weight. It lives
   inside the display (never the input): it is how the app refers to this
   chamber, not part of the name anyone typed. */
.reg-leaf-ord {
  margin-left: 8px;
  font-size: 1.1rem;
  color: var(--bb-vermillion);
  vertical-align: 0.35em;
}
.reg-leaf-owner { color: var(--bb-ink-faint); }
/* The place block reads ACROSS, not down. Stacked (seal over plate over jump)
   it stood 236px tall beside a 74px versal and a 52px title, so the head
   measured 251px to hold ~74px of content and the leaf opened on a column of
   whitespace. Laid out as seal-beside-its-captions it is ~112px — near enough
   the versal's own height that the head has nothing left to pad. */
.reg-leaf-place {
  flex: 0 0 auto;
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: center;
  gap: 2px 12px;
  /* Sized BY its contents, not to a guessed width. A fixed 320px left the
     caption column ~202px for a jump control whose intrinsic width is 224px,
     and since the block is `flex: 0 0 auto` the excess didn't shrink anything —
     it spilled 22px past the leaf's right edge, the one element in the chapter
     not respecting the margin. `max-content` makes the jump the sizing
     authority, so it can neither wrap nor overflow whatever the theme's face
     costs. The floor name is capped separately below so a long one ellipsizes
     rather than widening the whole margin. */
  width: max-content;
  /* The seal sits OUTERMOST, hard against the leaf's right margin, with the
     floor name and its jump right-aligned into the gap beside it. That puts a
     drawn mark at each end of the head — the heraldic versal on the left, the
     plan seal on the right — with the title spanning between them, instead of
     the head trailing off into a caption column. */
  text-align: right;
}
.reg-leaf-seal {
  grid-column: 2;
  grid-row: 1 / 3;
  border: 1px solid var(--bb-rule-soft);
  background: var(--bb-parchment-light);
  padding: 4px;
  line-height: 0;
}
.reg-leaf-place.is-offmap {
  display: block;
  width: auto;
}
.reg-leaf-place.is-offmap .reg-offmap { margin-top: 4px; }
.reg-leaf-plate {
  grid-column: 1;
  grid-row: 1;
  align-self: end;
  justify-self: end;
  max-width: 240px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-family: var(--bb-font-display);
  font-size: 0.74rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--bb-ink-soft);
}
/* Sized to its own text now that the column is wide enough not to wrap it —
   stretched across a 160px column it was a two-line 48px slab. */
.reg-leaf-place .reg-show-on-plate {
  grid-column: 1;
  grid-row: 2;
  align-self: start;
  justify-self: end;
  white-space: nowrap;
}
/* The reading block hangs off the TITLE's axis — 90px is the versal (74) plus
   the head's own gap (16) — so it and the verbs stop sharing a left edge and
   stop reading as one stack. The separation is an axis, not a gap: the verbs
   stay at the leaf's edge as marginal apparatus and the prose starts inside a
   margin. This is a deliberate break from the app-wide habit of aligning body
   content to its header's left edge, and the only place that break is made.
   Recorded in DESIGN_SPEC_core.md rather than left implicit.

   The rule lives HERE, not on .reg-actions, and that is the whole trick: a
   basic room's leaf is head + verbs and nothing else, so it has no body and
   draws no rule. Hang it off the actions instead and a basic room gets a
   hairline under empty parchment — the exact "double divider" defect the
   .reg-actions comment above was written to fix, re-introduced one element
   lower — and the read-only viewer, which drops .reg-actions entirely, would
   need a :has() special case to avoid the same thing. */
.reg-leaf-body {
  margin: 16px 0 0 90px;
  padding-top: 22px;
  border-top: 1px solid var(--bb-rule);
}
/* The marginal note. Struck vermillion at the head of the leaf, because a
   chamber that fails its requirement should say so before it says what it
   does. */
.reg-leaf-unmet {
  margin-bottom: 14px;
  padding: 8px 12px;
  background: color-mix(in srgb, var(--bb-vermillion) 7%, transparent);
  border-left: 3px solid var(--bb-vermillion);
}
.reg-leaf-unmet h3 {
  margin: 0 0 3px;
  font-family: var(--bb-font-display);
  font-size: 0.7rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--bb-vermillion-deep);
}
.reg-leaf-unmet ul {
  list-style: none;
  margin: 0;
  padding: 0;
}
.reg-leaf-unmet li {
  font-family: var(--bb-font-italic);
  font-size: 0.88rem;
  line-height: 1.4;
  color: var(--bb-ink-soft);
}

/* ---- Register codex: the front matter (#300) ---------------------------- */
.reg-front { max-width: 62ch; }
.reg-front-sub {
  margin: 4px 0 20px;
  font-family: var(--bb-font-italic);
  font-size: 0.95rem;
  color: var(--bb-ink-soft);
}
.reg-front-plates { margin-bottom: 20px; }
.reg-front-row {
  display: flex;
  align-items: baseline;
  gap: 10px;
  padding: 5px 0;
  border-bottom: 1px solid var(--bb-rule-soft);
}
.reg-front-plate {
  flex: 1 1 auto;
  font-family: var(--bb-font-display);
  font-size: 0.92rem;
  color: var(--bb-ink);
}
.reg-front-count {
  flex: 0 0 auto;
  font-size: 0.86rem;
  color: var(--bb-ink-faint);
}
.reg-front-hint {
  font-family: var(--bb-font-italic);
  font-size: 0.9rem;
  line-height: 1.5;
  color: var(--bb-ink-faint);
}
/* Trailing add-tile (steward ruling): a dashed gilt affordance filling one grid
 * cell, replacing the old header Commission button. Doubles as the empty-state
 * CTA. One per grid (Commission for specials, Add basic room for basics). */
.reg-card-add {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 64px;
  padding: 14px;
  background: transparent;
  border: 1px dashed var(--bb-rule);
  box-shadow: none;
  cursor: pointer;
  font-family: var(--bb-font-display);
  font-size: 0.82rem;
  letter-spacing: 0.04em;
  color: var(--bb-gilt-deep);
}
.reg-card-add:hover {
  background: var(--bb-gilt-fill);
  border-color: var(--bb-gilt);
}
.reg-card-add:focus-visible {
  outline: 2px solid var(--bb-vermillion);
  outline-offset: 2px;
}
/* At the special-facility cap the Commission tile is greyed and inert — raise a
 * character level or remove a facility to add another. Kept hoverable (aria-
 * disabled, not [disabled]) so its explanatory tooltip still fires. */
.reg-card-add.is-at-cap {
  color: var(--bb-ink-faint);
  border-color: var(--bb-rule-soft);
  cursor: not-allowed;
  opacity: 0.65;
}
.reg-card-add.is-at-cap:hover {
  background: transparent;
  border-color: var(--bb-rule-soft);
}

/* `.reg-card` survives only as the add-tile's base (see .reg-card-add above);
   the chamber card itself became the leaf. */
.reg-card {
  display: flex;
  flex-direction: column;
  background: var(--bg-card);
  border: 1px solid var(--bb-rule);
  box-shadow: var(--shadow-card);
}
/* Grid-stretched cards can run taller than their own content (siblings in the
 * row carry more optional text) — as a flex column the head sits at its own
 * height and the card's background fills the rest, instead of leaving a
 * plain-block auto-height box whose fill below the content read as a seam. */
.reg-leaf.is-unplaced .reg-leaf-head { border-left: 3px solid var(--bb-vermillion); padding-left: 12px; }

/* Under-construction strip (#195) — sits between the card head (itself a
   <button>, can't nest controls) and the panel, so the one-click finish
   stays reachable without expanding the card. Gilt: a "next move," not an
   alert (vermillion stays reserved for the unplaced marker). */
.reg-construction-strip {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  padding: 6px 14px;
  background: var(--bb-gilt-fill);
  border-top: 1px solid var(--bb-gilt);
  border-bottom: 1px solid var(--bb-gilt);
}
.reg-construction-label {
  font-family: var(--bb-font-display);
  font-size: 0.78rem;
  letter-spacing: 0.02em;
  color: var(--bb-gilt-deep);
}
.reg-construction-finish {
  flex: 0 0 auto;
  padding: 4px 10px;
  font-size: 0.8rem;
}

/* Facility-condition strip (#250) — lingering event consequences below the
   head, beside the construction strip (the head is a <button>, can't nest the
   × control). Valence-coloured pills in the flag-chip idiom (border + text, no
   fill — matching .reg-haunted / .reg-offmap): adverse = vermillion, boon =
   gilt. Each carries an inline hand-clear ×. */
.reg-conditions {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  padding: 6px 14px;
  border-top: 1px solid var(--bb-rule-soft);
}
.reg-condition {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 1px 3px 1px 7px;
  font-size: 0.72rem;
  letter-spacing: 0.02em;
  border: 1px solid;
  border-radius: 2px;
}
.reg-condition.is-adverse { color: var(--bb-vermillion-deep); border-color: var(--bb-vermillion); }
.reg-condition.is-boon { color: var(--bb-gilt-deep); border-color: var(--bb-gilt); }
.reg-condition-text { line-height: 1.3; }
.reg-condition-clear {
  flex: 0 0 auto;
  display: inline-grid;
  place-items: center;
  width: 18px;
  height: 18px;
  padding: 0;
  font-size: 14px;
  line-height: 1;
  color: inherit;
  background: transparent;
  border: 0;
  border-radius: 2px;
  cursor: pointer;
  opacity: 0.7;
}
.reg-condition-clear:hover { opacity: 1; background: var(--bb-gilt-fill); }
@media (pointer: coarse) {
  .reg-condition-clear { width: 26px; height: 26px; font-size: 16px; }
}

.reg-glyph {
  flex: 0 0 auto;
  width: 34px;
  height: 34px;
  display: grid;
  place-items: center;
  color: var(--bb-ink-soft);
}
.reg-glyph .glyph-svg { width: 26px; height: 26px; }
.reg-name {
  font-family: var(--bb-font-display);
  font-size: 1rem;
  color: var(--bb-ink);
  line-height: 1.2;
}
.reg-kind {
  font-size: 0.78rem;
  color: var(--bb-ink-faint);
  letter-spacing: 0.02em;
  margin-top: 1px;
}
/* The catalog type (shown only when the facility is custom-named) carries a
   touch more weight than the surrounding faint metadata — identity, not a
   capability, so monochrome (never gilt). */
.reg-kind-type { color: var(--bb-ink-soft); }
.reg-haunted {
  display: inline-block;
  margin-left: 6px;
  padding: 0 5px;
  font-size: 10px;
  letter-spacing: 0.04em;
  border: 1px solid var(--bb-haunted);
  border-radius: 2px;
  color: var(--bb-haunted);
}
/* Owner chip (nit 6): always-visible on a special card's head when the bastion
 * is shared, so "who owns what" reads at a glance. Identity, not capability —
 * monochrome on parchment, never gilt. Stacked under the name in the head-text
 * column (not raced horizontally against it in the head's flex row) — a fixed-
 * width sibling there squeezed the title/meta text down to a sliver, and a
 * long character name overran the card entirely; max-width + ellipsis here
 * cap it to the column's own width instead. */
.reg-owner-chip {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  max-width: 100%;
  margin-top: 3px;
  padding: 2px 8px;
  overflow: hidden;
  font-family: var(--bb-font-display);
  font-size: 0.7rem;
  letter-spacing: 0.04em;
  white-space: nowrap;
  text-overflow: ellipsis;
  color: var(--bb-ink-soft);
  background: var(--bb-parchment-mid);
  border: 1px solid var(--bb-rule-soft);
}
.reg-order-line {
  margin-top: 5px;
  font-size: 0.9rem;
  color: var(--bb-gilt-deep);
  font-weight: 600;
}
.reg-loc {
  margin-top: 4px;
  font-size: 0.78rem;
  color: var(--bb-ink-faint);
}
/* Off-map (#195) — neutral, distinct from both the vermillion "not placed
   yet" marker and the gilt haunted/construction chips: this is a deliberate,
   resolved location, not a warning or a next-move prompt. Sapphire matches
   the shelf chip treatment (render-sidebar.js). */
.reg-offmap-line {
  display: flex;
  align-items: center;
  gap: 6px;
  margin-top: 5px;
}
/* `flex: 0 0 auto` + `nowrap` are both load-bearing, not tidiness: as a
   shrinkable flex child this 10px seven-character badge was being taken to
   min-content and breaking at its own hyphen — "Off-" over "map", 44x31px — at
   every viewport, on the only off-map badge the chapter has. */
.reg-offmap {
  display: inline-block;
  flex: 0 0 auto;
  white-space: nowrap;
  padding: 0 5px;
  font-size: 10px;
  letter-spacing: 0.04em;
  border: 1px solid var(--bb-sapphire);
  border-radius: 2px;
  color: var(--bb-sapphire-deep);
}
.reg-offmap-line .reg-loc { margin-top: 0; }
.reg-unplaced {
  display: inline-block;
  margin-top: 5px;
  font-size: 0.8rem;
  color: var(--bb-vermillion);
  font-family: var(--bb-font-italic);
}
/* The leaf's two reading columns. `auto-fit` + `minmax(min(100%, 20rem), 1fr)`
   rather than fixed flex bases: at ~780px the fixed pair squeezed the prose
   column to ~90px and took the page to 2950px. Folding them intrinsically
   brings that to 1404px with no viewport rule at all, which is how this
   chapter earns its "no new breakpoint". */
.reg-panel-cols {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 20rem), 1fr));
  gap: 4px 28px;
  align-items: start;
}
.reg-col-prose {
  min-width: 0;
  max-width: 64ch;
}
.reg-col-facts {
  min-width: 0;
  max-width: 360px;
}
/* Both columns start flush with the top of the panel so they read as a row. */
.reg-hero { margin: 0 0 14px; }
.reg-col-facts > :first-child { margin-top: 0; }
.reg-flavor {
  margin: 0 0 10px;
  font-family: var(--bb-font-italic);
  font-size: 0.92rem;
  line-height: 1.45;
  color: var(--bb-ink-soft);
}
.reg-order-card {
  background: var(--bb-gilt-fill);
  /* Boxed (not a left rail) so it never competes with the card-level
     vermillion left rail on an expanded unplaced facility. */
  border: 1px solid var(--bb-gilt);
  padding: 8px 12px;
}
.reg-order-card-head {
  font-family: var(--bb-font-display);
  font-size: 0.72rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--bb-gilt-deep);
  margin-bottom: 3px;
}
.reg-order-card-body {
  margin: 0;
  font-size: 0.92rem;
  line-height: 1.45;
  color: var(--bb-ink);
}


/* ---- Register codex: responsive (#300) ---------------------------------- */
/* LAST in the register cluster, after every base rule these override — a media
 * query adds no specificity, so a tier rule and the unconditional rule it
 * overrides are decided purely by source order. Above the tier this block is
 * inert and both panes are on screen together.
 *
 * SINGLE-SURFACE, so it lives here and not in the cross-surface band: every
 * selector below is a `.reg-*` inside this one chapter. The band is for rules
 * that coordinate several components at once, and adding a single-surface rule
 * to it is the mistake that block's header warns about.
 *
 * The tier predicate is asserted byte-identical against constants.js's
 * PHONE_TIER_QUERY by validate_styles.py — do not retype it by hand. */
.reg-leaf-back { display: none; }

@media (max-width: 700px), (max-height: 500px) and (pointer: coarse) {
  /* Two pages, not two panes. The index IS the phone's landing page, which is
   * why the front matter simply doesn't appear here: its job — how big is this
   * place, what are the plates — is what a floor-grouped index already shows,
   * and a title page you must scroll past to reach the index would be chrome. */
  .reg-codex {
    grid-template-columns: 1fr;
    gap: 0;
  }
  .reg-index {
    position: static;
    max-height: none;
    overflow-y: visible;
    border-right: 0;
    padding-right: 0;
  }
  .reg-codex.is-on-leaf .reg-index { display: none; }
  .reg-codex:not(.is-on-leaf) .reg-leaf-host { display: none; }

  .reg-leaf-back {
    display: block;
    width: 100%;
    text-align: left;
    margin-bottom: 10px;
    padding: 0 4px;
    background: transparent;
    border: 0;
    box-shadow: none;
    font-family: var(--bb-font-display);
    font-size: 0.82rem;
    letter-spacing: 0.04em;
    color: var(--bb-gilt-deep);
    cursor: pointer;
  }
  /* The versal and its seal stop being margin furniture and stack, so neither
   * squeezes the title into a two-character column. */
  .reg-leaf-head {
    flex-wrap: wrap;
    gap: 10px;
  }
  .reg-leaf-title { font-size: 1.45rem; }
  /* No inset axis on a phone: the leaf has the whole width and a 90px gutter
     would be a tenth of it. The rule and its breathing room stay. */
  .reg-leaf-body { margin-left: 0; margin-top: 14px; padding-top: 18px; }
  .reg-leaf .reg-glyph,
  .reg-leaf .reg-glyph .glyph-svg { width: 52px; height: 52px; }
  /* Same shape as the desktop block (seal beside its captions) — only wider,
     since it has the whole column instead of a margin. */
  .reg-leaf-place { width: 100%; }
  .reg-leaf-seal svg { width: 88px; height: 88px; }
}

/* ---- Dice roll tokens (#149) -------------------------------------------- */
/* Clickable dice — Beyond20 forwarding (#145). Surface-neutral names (not
   reg-*) so the affordance travels to the handout / bastion events later.
   A literal notation roll keeps its ink and wears a gilt dotted underline —
   the lowest-ink "rollable" mark that survives inside justified prose without
   breaking the line box. Gilt = an available reference action; vermillion is
   reserved for the active press (per the red-vs-gilt rule). */
.roll-die {
  display: inline;
  background: none;
  border: 0;
  padding: 0;
  font: inherit;
  color: inherit;
  text-decoration: underline dotted var(--bb-gilt);
  text-underline-offset: 2px;
  cursor: pointer;
}
.roll-die:hover,
.roll-die:focus-visible { text-decoration-style: solid; color: var(--bb-gilt-deep); }
.roll-die:active { color: var(--bb-vermillion); }
.roll-die:focus-visible { outline: 2px solid var(--bb-vermillion); outline-offset: 2px; }

/* The "roll a die" / odd-even phrase has no notation, so instead of underlining
   words (which would mislead about *which* die fires) we append a small mono
   chip showing the default d20 — a distinct shape + font that names the die. */
.roll-die-chip {
  display: inline-block;
  margin-left: 6px;
  padding: 0 6px;
  border: 1px solid var(--bb-gilt);
  border-radius: 2px;
  background: none;
  color: var(--bb-gilt-deep);
  font-family: var(--bb-font-mono);
  font-size: 0.78em;
  cursor: pointer;
}
.roll-die-chip:hover,
.roll-die-chip:focus-visible { color: var(--bb-vermillion); border-color: var(--bb-vermillion); }
.roll-die-chip:active { color: var(--bb-vermillion-deep); }
.roll-die-chip:focus-visible { outline: 2px solid var(--bb-vermillion); outline-offset: 2px; }


/* ---- SRD reference glyphs + stat cards (#213) --------------------------- */
/* Open5e stat-reference glyph + card (#213). The open-book glyph is a quiet
   inline sibling of .roll-die — it trails a creature/spell name and only
   renders where the lookup will resolve (never a dead control). Rest state
   inherits the surrounding ink; gilt names the hover/focus affordance. */
.ref-glyph {
  display: inline-block;
  vertical-align: -0.08em;
  width: 0.85em;
  height: 0.85em;
  margin-left: 4px;
  padding: 0;
  border: 0;
  background: none;
  color: inherit;
  line-height: 1;
  cursor: pointer;
}
.ref-glyph .glyph-svg { display: block; width: 100%; height: 100%; }
.ref-glyph:hover,
.ref-glyph:focus-visible { color: var(--bb-gilt-deep); }
.ref-glyph:active { color: var(--bb-vermillion); }
.ref-glyph:focus-visible { outline: 2px solid var(--bb-vermillion); outline-offset: 2px; }
/* Companion slot beside a folded option <select> — holds the glyph for the
   currently chosen creature (a button can't live inside an <option>). */

/* The reference card: an anchored popover (desktop) that turns into a bottom
   sheet ≤700px. Header + colophon stay fixed; the stat body scrolls. */
.ref-card {
  position: fixed;
  z-index: 60;
  display: flex;
  flex-direction: column;
  min-width: 260px;
  max-width: 360px;
  max-height: min(70vh, 560px);
  padding: 10px 14px;
  background: var(--bb-parchment-light);
  border: 1px solid var(--bb-rule);
  border-radius: 0;
  box-shadow: 0 10px 24px -8px var(--bb-shadow-mid);
}
.ref-card-head {
  display: flex;
  align-items: baseline;
  gap: 8px;
}
.ref-card-title {
  flex: 1 1 auto;
  margin: 0;
  font-family: var(--bb-font-display);
  font-size: 1.05rem;
  font-weight: 600;
  letter-spacing: 0.02em;
  outline: none;
}
.ref-card-close {
  flex: 0 0 auto;
  padding: 0 4px;
  border: 0;
  background: none;
  color: var(--bb-ink-faint);
  font-size: 1.1rem;
  line-height: 1;
  cursor: pointer;
}
.ref-card-close:hover,
.ref-card-close:focus-visible { color: var(--bb-vermillion); }
.ref-card-close:focus-visible { outline: 2px solid var(--bb-vermillion); outline-offset: 2px; }
.ref-card-body {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  padding-right: 12px;
  scrollbar-gutter: stable;
}
.ref-meta {
  margin: 2px 0 6px;
  font-family: var(--bb-font-italic);
  font-style: italic;
  font-size: 0.86rem;
  color: var(--bb-ink-faint);
}
.ref-section {
  margin: 10px 0 2px;
  font-family: var(--bb-font-display);
  font-size: 0.7rem;
  font-weight: 600;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--bb-ink-faint);
  border-bottom: 1px solid var(--bb-rule-soft);
}
.ref-entry {
  margin: 6px 0;
  font-size: 0.86rem;
}
/* An attack's bold lead-in name doubling as the one-click attack+damage roll —
   the .roll-die gilt-underline grammar, kept bold like its <strong> sibling. */
.ref-attack { font-weight: 600; }
.ref-card-note {
  margin: 8px 0;
  font-family: var(--bb-font-italic);
  font-style: italic;
  color: var(--bb-ink-faint);
}
/* AC/HP/Speed rows reuse .reg-detail; the tighter label column suits the
   short stat words better than the register's 88px. */
.ref-card .reg-detail-label { flex-basis: 76px; }
.ref-card-footer {
  display: flex;
  align-items: baseline;
  gap: 10px;
  margin-top: 8px;
  padding-top: 8px;
  border-top: 1px dashed var(--bb-rule);
}
/* Attribution colophon — renders in every card state (CC-BY requirement). */
.ref-card-colophon {
  display: flex;
  gap: 10px;
  margin-left: auto;
  font-size: 0.72rem;
  color: var(--bb-ink-faint);
  white-space: nowrap;
}
.ref-card-colophon a {
  color: inherit;
  text-decoration: underline dotted var(--bb-rule-soft);
  text-underline-offset: 2px;
}
.ref-card-colophon a:hover,
.ref-card-colophon a:focus-visible { color: var(--bb-gilt-deep); }
/* Tier query (#283): the sheet is the phone's answer in both orientations — see
   positionCard() in open5e.js for why anchoring loses at 390px tall. The
   landscape block at the end of this sheet caps its width. */
@media (max-width: 700px), (max-height: 500px) and (pointer: coarse) {
  /* Bottom sheet: an anchored popover beside a tapped word is cramped on a
     phone — pin the card full-width to the bottom edge instead. The JS skips
     inline left/top positioning when it applies this class. */
  .ref-card--sheet {
    left: 0;
    right: 0;
    top: auto;
    bottom: 0;
    min-width: 0;
    max-width: none;
    max-height: 80vh;
    border-radius: 10px 10px 0 0;
    box-shadow: 0 -10px 24px -8px var(--bb-shadow-mid);
  }
}


/* ---- Name-suggestion popover (#208) ------------------------------------- */
/* Name-suggester popover (#208) — clones the .ref-card anchored-popover idiom:
   fixed parchment card that flips above when it'd overflow and becomes a bottom
   sheet ≤700px. The style select doubles as the header (no title bar). */
.name-suggest-card {
  position: fixed;
  z-index: 60;
  display: flex;
  flex-direction: column;
  gap: 6px;
  min-width: 200px;
  max-width: 260px;
  padding: 10px 12px;
  background: var(--bb-parchment-light);
  border: 1px solid var(--bb-rule);
  border-radius: 0;
  box-shadow: 0 10px 24px -8px var(--bb-shadow-mid);
}
.name-suggest-head {
  display: flex;
  align-items: center;
  gap: 8px;
}
/* The style select IS the header — reuses the manuscript-select grammar (small
   caps display face, dotted gilt underline) so it reads as parchment chrome. */
.name-suggest-style {
  flex: 1 1 auto;
  min-width: 0;
  padding: 2px 2px 3px;
  border: 0;
  border-bottom: 1px dotted var(--bb-gilt);
  background: none;
  font-family: var(--bb-font-display);
  font-size: 0.9rem;
  letter-spacing: 0.02em;
  color: var(--bb-ink);
  cursor: pointer;
}
.name-suggest-style:hover { border-bottom-color: var(--bb-gilt-deep); }
.name-suggest-style:focus-visible { outline: 2px solid var(--bb-gilt); outline-offset: 2px; }
/* Animal-recruit lock: the picker is replaced by a static kicker. */
.name-suggest-kicker {
  flex: 1 1 auto;
  font-family: var(--bb-font-display);
  font-size: 0.7rem;
  font-weight: 600;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--bb-ink-faint);
}
.name-suggest-close {
  flex: 0 0 auto;
  padding: 0 4px;
  border: 0;
  background: none;
  color: var(--bb-ink-faint);
  font-size: 1.1rem;
  line-height: 1;
  cursor: pointer;
}
.name-suggest-close:hover,
.name-suggest-close:focus-visible { color: var(--bb-vermillion); }
.name-suggest-close:focus-visible { outline: 2px solid var(--bb-vermillion); outline-offset: 2px; }
.name-suggest-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
}
/* Candidate rows — the names are the star; borrow the .house-role gilt-left-rule
   grammar so the picks feel native to the Household card family. */
.name-suggest-pick {
  width: 100%;
  text-align: left;
  padding: 0.3rem 0.4rem;
  border: 0;
  border-left: 2px solid transparent;
  background: none;
  font-family: var(--bb-font-display);
  font-size: 1.05rem;
  color: var(--bb-ink);
  cursor: pointer;
}
.name-suggest-pick:hover,
.name-suggest-pick:focus-visible {
  background: var(--bb-parchment);
  border-left-color: var(--bb-gilt-deep);
  outline: none;
}
.name-suggest-pick:focus-visible { outline: 2px solid var(--bb-gilt); outline-offset: -2px; }
.name-suggest-foot {
  display: flex;
  justify-content: flex-end;
  margin-top: 2px;
  padding-top: 6px;
  border-top: 1px dashed var(--bb-rule);
}
/* Reroll — quiet, secondary to picking; the dice glyph names the "roll again"
   action (the quill trigger inscribes; the dice reroll). */
.name-suggest-reroll {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 2px 4px;
  border: 0;
  background: none;
  font-family: var(--bb-font-display);
  font-size: 0.82rem;
  letter-spacing: 0.03em;
  color: var(--bb-ink-soft);
  cursor: pointer;
}
.name-suggest-reroll:hover,
.name-suggest-reroll:focus-visible { color: var(--bb-gilt-deep); }
.name-suggest-reroll:focus-visible { outline: 2px solid var(--bb-gilt); outline-offset: 2px; }
.name-suggest-dice { display: inline-flex; width: 1em; height: 1em; }
.name-suggest-dice .glyph-svg { display: block; width: 100%; height: 100%; }
/* Tier query (#283), same reasoning as .ref-card--sheet above. */
@media (max-width: 700px), (max-height: 500px) and (pointer: coarse) {
  .name-suggest-card--sheet {
    left: 0;
    right: 0;
    top: auto;
    bottom: 0;
    min-width: 0;
    max-width: none;
    border-radius: 10px 10px 0 0;
    box-shadow: 0 -10px 24px -8px var(--bb-shadow-mid);
  }
  .name-suggest-card--sheet .name-suggest-pick { padding: 0.5rem 0.4rem; }
  .house-typeahead--sheet {
    left: 0;
    right: 0;
    top: auto;
    bottom: 0;
    min-width: 0;
    max-width: none;
    border-radius: 10px 10px 0 0;
    box-shadow: 0 -10px 24px -8px var(--bb-shadow-mid);
  }
  .house-typeahead--sheet .name-suggest-list { max-height: 45vh; }
}

/* CREATURE TYPEAHEAD (#218) — the suggestions under a statted-as field. It
   borrows `.name-suggest-card`'s chrome and pick grammar outright rather than
   growing a second popover look: a native <datalist> was the first fit and had
   to go, because its popup is browser chrome no CSS can reach, and replacing
   one unstyled list with a differently-styled one would be no better.
   The differences are only what the content asks for — the list can run to 35
   entries, so it scrolls, and a creature name is data rather than the star a
   suggested NAME is, so it sits a step down in size. */
.house-typeahead .name-suggest-list {
  max-height: 15rem;
  overflow-y: auto;
  padding-right: 12px;
  scrollbar-gutter: stable;
}
/* Type matched to `.house-statted` exactly — same size, same small caps, same
   tracking — so a suggestion reads as the words that will land in the field
   rather than as a menu about them. (A suggested NAME in the name popover is
   the star of its own card and sits larger; a creature here is the field's own
   value previewed.) Touch targets are unaffected: these are real buttons, so
   the global `pointer: coarse` 44px floor still applies. */
.house-typeahead .name-suggest-pick {
  font-size: 0.84rem;
  font-variant: small-caps;
  letter-spacing: 0.03em;
}


/* ---- Register card detail ----------------------------------------------- */
.reg-detail {
  display: flex;
  gap: 8px;
  padding: 3px 0;
  font-size: 0.86rem;
}
.reg-detail-label {
  flex: 0 0 88px;
  color: var(--bb-ink-faint);
  font-family: var(--bb-font-display);
  font-size: 0.7rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  padding-top: 2px;
}
.reg-detail-value { flex: 1 1 auto; color: var(--bb-ink); min-width: 0; }
.reg-fold {
  margin-top: 10px;
  padding-top: 10px;
  border-top: 1px dashed var(--bb-rule);
}
.reg-full-rules {
  margin-top: 12px;
  border-top: 1px solid var(--bb-rule);
  padding-top: 8px;
}
.reg-full-rules > summary {
  cursor: pointer;
  font-family: var(--bb-font-display);
  font-size: 0.72rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--bb-ink-soft);
  list-style-position: inside;
}
.reg-full-rules > summary:hover { color: var(--bb-ink); }
.reg-full-rules > summary:focus-visible {
  outline: 2px solid var(--bb-gilt);
  outline-offset: 2px;
}
.reg-full-rules[open] > summary { color: var(--bb-ink); margin-bottom: 6px; }
.reg-full-rules-body p {
  margin: 0 0 8px;
  font-size: 0.88rem;
  line-height: 1.5;
  color: var(--bb-ink-soft);
}
.reg-full-rules-body p:last-child { margin-bottom: 0; }
.reg-rules-lead { margin-bottom: 4px !important; }
.reg-rules-list {
  margin: 0 0 10px;
  padding-left: 20px;
  list-style: disc;
}
.reg-rules-list li {
  margin: 0 0 4px;
  font-size: 0.88rem;
  line-height: 1.5;
  color: var(--bb-ink-soft);
}
.reg-rules-list li:last-child { margin-bottom: 0; }
.reg-full-rules-body strong { color: var(--bb-ink); font-weight: 600; }
.reg-rules-source {
  margin-top: 12px !important;
  padding-top: 8px;
  border-top: 1px dashed var(--bb-rule);
  font-size: 0.82rem;
  color: var(--bb-ink-faint);
}
.reg-rules-source strong { color: var(--bb-ink-faint); }
.reg-show-on-plate {
  /* Sits directly under the Location row it acts on — grouped, not stranded. */
  margin-top: 6px;
  padding: 6px 14px;
  background: transparent;
  border: 1px solid var(--bb-gilt);
  color: var(--bb-gilt-deep);
  font-family: var(--bb-font-display);
  font-size: 0.74rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  cursor: pointer;
}
.reg-show-on-plate:hover { background: var(--bb-gilt-fill); }
.reg-show-on-plate:focus-visible {
  outline: 2px solid var(--bb-gilt);
  outline-offset: 2px;
}

/* Read-only Stores echo: grouped current-state facts + one nav link out to the
   editable Stores dialog. No inputs, no gilt box — these read as facts. */
.reg-stores {
  margin-top: 12px;
  padding-top: 10px;
  border-top: 1px dashed var(--bb-rule);
}
.reg-stores-head {
  font-family: var(--bb-font-display);
  font-size: 0.68rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--bb-ink-faint);
  margin-bottom: 6px;
}
.reg-num-max { color: var(--bb-ink-faint); }
.reg-note-echo { font-family: var(--bb-font-italic); font-style: italic; }
.reg-manage-stores {
  margin-top: 6px;
  padding: 2px 0;
  background: none;
  border: 0;
  color: var(--bb-gilt-deep);
  font-family: var(--bb-font-text);
  font-size: 0.84rem;
  cursor: pointer;
}
.reg-manage-stores:hover { text-decoration: underline; }
.reg-manage-stores:focus-visible {
  outline: 2px solid var(--bb-gilt);
  outline-offset: 2px;
}
/* The Hirelings fact-row value doubles as the jump to the Household gallery
   (#154) — roll-die link idiom: the dotted gilt underline carries it, the
   names stay ink. */
.reg-household-jump {
  display: inline;
  padding: 0;
  background: none;
  border: 0;
  font: inherit;
  color: inherit;
  text-align: left;
  cursor: pointer;
  text-decoration: underline dotted var(--bb-gilt);
  text-underline-offset: 2px;
}
.reg-household-jump:hover,
.reg-household-jump:focus-visible { text-decoration-style: solid; color: var(--bb-gilt-deep); }
.reg-household-jump:focus-visible { outline: 2px solid var(--bb-gilt); outline-offset: 2px; }

/* Stack the leaf's short label/value rows on narrow screens. The two card-grid
   rules that used to lead this block went with the grid (#300). */
@media (max-width: 640px) {
  .reg-detail { flex-direction: column; gap: 1px; }
  .reg-detail-label { flex-basis: auto; }
}

/* ---- Plate-spine: single-active-floor switcher (#160, §5.9) ------------- */
/* Left rail of one vellum plate per floor (high→basement), bracketed by dashed
 * + buttons. Retires the per-floor .floor-side gutter — floor verbs moved to
 * the plate context menu (render-spine.js). Slice C adds the vellum polish. */

/* The spine lives inside .stage-body (below the command rail), sticky in BOTH
 * axes so it stays pinned when a wide map scrolls horizontally and when a tall
 * one scrolls vertically (#218 — the floor you are on is a control you need
 * most while you are deep in a plan, and it used to leave with the scroll,
 * forcing a round trip to the top and back to change floors). Its parchment
 * backing keeps the scrolled canvas from bleeding beneath it. align-self:
 * flex-start so it's only as tall as its plates, not stretched to the canvas
 * height — which is also what leaves it room to travel. A spine taller than
 * the pane scrolls with the page from its foot, the ordinary sticky fallback:
 * the alternative is a second scroller inside a 116px column. */
.stage-body {
  display: flex;
  align-items: flex-start;
  gap: 12px;
  padding-top: 16px;
  min-width: 0;
}
.floor-spine {
  position: sticky;
  left: 0;
  top: 0;
  z-index: 5;
  align-self: flex-start;
  flex: 0 0 116px;
  display: flex;
  flex-direction: column;
  gap: 8px;
  padding: 2px 4px;
  background: var(--bb-parchment);
}
.floor-spine-plates {
  display: flex;
  flex-direction: column;
  gap: 6px;
  min-height: 0;
}

/* One plate. Quiet by default; the active plate lifts + gains a vermillion
 * inboard edge-bar (a fill would read as an armed .mode-btn — precedent:
 * .bastions-menu-row.current .bm-load). */
.floor-plate {
  position: relative;
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 2px;
  min-height: 52px;
  padding: 9px 8px 9px 11px;
  /* Vellum card: a faint top-lit parchment gradient + inset sheen. */
  background: linear-gradient(180deg, var(--bb-parchment-light) 0%, var(--bb-parchment) 100%);
  border: 1px solid var(--bb-rule-soft);
  border-top-color: var(--bb-inset-light);
  box-shadow: inset 0 1px 0 var(--bb-inset-light);
  cursor: pointer;
  text-align: left;
  color: var(--bb-ink-soft);
  transition: background 0.12s, color 0.12s;
}
.floor-plate:hover { background: var(--bb-parchment-light); }
.floor-plate:focus-visible {
  outline: none;
  box-shadow: inset 0 0 0 2px var(--bb-vermillion);
}
.floor-plate.is-active {
  background: var(--bb-parchment-light);
  color: var(--bb-ink);
}
.floor-plate.is-active::after {
  content: "";
  position: absolute;
  top: 0; right: 0; bottom: 0;
  width: 3px;
  background: var(--bb-vermillion-deep);
}

.plate-eyebrow {
  font-family: var(--bb-font-display);
  font-size: 0.625rem;              /* 10px */
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--bb-ink-faint);
  line-height: 1.2;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.floor-plate.is-active .plate-eyebrow { color: var(--bb-vermillion); }
/* Vehicles have no structural word — drop the empty eyebrow line. */
.plate-eyebrow:empty { display: none; }

.plate-label {
  font-family: var(--bb-font-display);
  /* 14px, not 15: the plate's text box is ~87px, and one step down is what puts
   * the ordinary long floor name ("Underground", "Substructure") on a single
   * line instead of over two. Measured, not guessed. */
  font-size: 0.875rem;              /* 14px */
  line-height: 1.15;
  color: inherit;
  /* Clamp to 2 lines; the full name rides the tooltip. */
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  /* NEVER break inside a word. A name too wide for the plate ellipsizes; it
   * does not split. "Undergroun / d" reads as a typo in the user's own name,
   * where "Wwwwwwww…" reads as what it is — a label too long to show, whose
   * full text is one hover away. Multi-word names still wrap at the space.
   * `overflow-wrap: break-word` is NOT a substitute: it also hard-breaks a word
   * that can't fit on a line of its own, which is exactly this case. */
  word-break: normal;
  overflow-wrap: normal;
  text-overflow: ellipsis;
}
/* An unnamed floor has no user-name line — collapse it so the plate shows just
 * the eyebrow (no empty gap). Renaming re-inflates it via the [contenteditable]
 * rule below. */
.plate-label:empty { display: none; }
/* Rename affordance — contenteditable is switched on only during a rename. */
.plate-label[contenteditable] {
  display: block;
  overflow: visible;
  /* While renaming you must see everything you type, so the read-state's
   * ellipsis is off (overflow: visible) — which means a long word has to wrap
   * here or it spills out of the spine and over the canvas. Wrapping anywhere
   * is the right trade for a transient editing state; the no-break rule above
   * governs the settled label. */
  overflow-wrap: anywhere;
  cursor: text;
  outline: none;
  background: var(--bb-parchment-light);
  box-shadow: 0 0 0 1px var(--bb-vermillion);
}

/* ⋯ verbs button on the active plate — persistent-but-faint (the only touch
 * path to floor verbs once the .floor-actions links are gone). */
.plate-menu-btn {
  position: absolute;
  top: 2px;
  right: 5px;
  width: 20px;
  height: 20px;
  padding: 0;
  line-height: 1;
  background: transparent;
  border: none;
  box-shadow: none;
  color: var(--bb-ink-faint);
  font-size: 1rem;
  cursor: pointer;
  opacity: 0.55;
}
.plate-menu-btn:hover { opacity: 1; color: var(--bb-vermillion); }

/* + Add-floor / + Add-basement — quiet dashed affordances at spine top & bottom,
 * glyph-only. */
.spine-add-btn {
  flex: 0 0 auto;
  padding: 8px 8px 8px 11px;
  text-align: left;
  background: transparent;
  border: 1px dashed var(--bb-rule);
  color: var(--bb-ink-soft);
  font-family: var(--bb-font-display);
  font-size: 0.72rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  line-height: 1.1;
  white-space: nowrap;
  cursor: pointer;
  box-shadow: none;
  border-radius: 0;
}
.spine-add-btn:hover {
  border-color: var(--bb-vermillion);
  color: var(--bb-vermillion);
  background: rgba(168,40,29,0.06);
}

/* Compact floor switcher — hidden on the wide layout (the plates are the
 * switcher there), shown ≤1099px in place of the plates. */
/* The wrapper gates visibility (hidden on desktop, shown ≤1099px); the button
 * itself is always block so it fills the wrap when shown. */
.floor-select-wrap { display: none; width: 100%; max-width: 260px; }
.floor-select {
  display: block;
  width: 100%;
  text-align: left;
  font-family: var(--bb-font-display);
  font-size: 0.8rem;
  /* A custom-dropdown trigger dressed as the old themed select: strip button
   * chrome and draw a hand-inked caret so it reads as a picker. Roomy padding
   * gives a ~40px thumb target at the phone tier (#218). */
  appearance: none;
  -webkit-appearance: none;
  padding: 9px 28px 9px 10px;
  border: 1px solid var(--bb-rule);
  border-radius: 0;
  box-shadow: none;
  background-color: var(--bb-parchment-light);
  background-image: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20width='10'%20height='7'%20viewBox='0%200%2010%207'%3E%3Cpath%20d='M1%201l4%204%204-4'%20fill='none'%20stroke='%236b5433'%20stroke-width='1.5'%20stroke-linecap='round'%20stroke-linejoin='round'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 10px center;
  color: var(--bb-ink);
  cursor: pointer;
}
/* The global `button:hover` uses the `background` *shorthand*, which resets
 * background-repeat to `repeat` and background-position to 0 0 — that tiles the
 * caret across the trigger into a chevron zigzag while the menu is open (#218,
 * the caret survives because the [aria-expanded] rule outspecifies the hover).
 * Re-assert the picker's own fill + caret on hover (these outspecify the global
 * chrome) so the caret stays a single mark and the parchment fill is kept. */
.floor-select:hover {
  background-color: var(--bb-parchment-light);
  background-image: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20width='10'%20height='7'%20viewBox='0%200%2010%207'%3E%3Cpath%20d='M1%201l4%204%204-4'%20fill='none'%20stroke='%236b5433'%20stroke-width='1.5'%20stroke-linecap='round'%20stroke-linejoin='round'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 10px center;
}
/* Caret flips up while the menu is open — the tell that this is a menu trigger,
 * not an inert field. Carries onto :hover so the open + hovered trigger keeps
 * the single up-caret rather than the down one (see the #218 note above). */
.floor-select[aria-expanded="true"],
.floor-select[aria-expanded="true"]:hover {
  background-image: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20width='10'%20height='7'%20viewBox='0%200%2010%207'%3E%3Cpath%20d='M1%205l4-4%204%204'%20fill='none'%20stroke='%236b5433'%20stroke-width='1.5'%20stroke-linecap='round'%20stroke-linejoin='round'/%3E%3C/svg%3E");
}
} /* @layer components — pause for the stray theme rule (#259) */
/* Light caret over the arcane theme's dark panel. Lives in the themes layer
   with the rest of the [data-theme] overrides. */
@layer themes {
[data-theme="arcane"] .floor-select,
[data-theme="arcane"] .floor-select:hover {
  background-image: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20width='10'%20height='7'%20viewBox='0%200%2010%207'%3E%3Cpath%20d='M1%201l4%204%204-4'%20fill='none'%20stroke='%23bfb8d6'%20stroke-width='1.5'%20stroke-linecap='round'%20stroke-linejoin='round'/%3E%3C/svg%3E");
}
[data-theme="arcane"] .floor-select[aria-expanded="true"],
[data-theme="arcane"] .floor-select[aria-expanded="true"]:hover {
  background-image: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20width='10'%20height='7'%20viewBox='0%200%2010%207'%3E%3Cpath%20d='M1%205l4-4%204%204'%20fill='none'%20stroke='%23bfb8d6'%20stroke-width='1.5'%20stroke-linecap='round'%20stroke-linejoin='round'/%3E%3C/svg%3E");
}
} /* @layer themes */
@layer components {
/* Pin the menu to the trigger's width (never narrower than its opener); grow to
 * fit the longest floor label, capped to the viewport. .bastions-menu already
 * gives max-height:60vh + overflow-y:auto, so a tall building scrolls inside.
 * Compound selector (.bastions-menu.floor-select-menu) so min-width:100% beats
 * the phone-tier `.bastions-menu { min-width:0 }` rule, which would otherwise
 * let the menu render narrower than its trigger on small phones. */
.bastions-menu.floor-select-menu {
  min-width: 100%;
  width: max-content;
  max-width: calc(100vw - 24px);
}
/* The plates→dropdown collapse moved to the ≤1024px Grounds tier (#275), where
 * the spine also turns horizontal — one shape change at one width, instead of
 * the spine collapsing at 1099 and everything else at 1024. 1025–1099 keeps its
 * vellum plates as a result. */

/* ---- Active floor frame ------------------------------------------------- */
/* #floor-stack now holds exactly one .plate-frame (the active floor). */
.floor-stack { gap: 0; }
.plate-frame {
  position: relative;
  display: flex;
  min-width: 0;
}

/* Read-only all-floors overview lightbox (#160 follow-up): the export-style
 * stack, captioned per floor, high→basement, inside a modal. The stack scrolls
 * WITHIN the dialog body — the hero canvas never re-stacks, so #160's no-scroll
 * editing win is preserved. */
/* .modal.overview-dialog, not .overview-dialog: the generic `.modal` rule
 * (padding:0; max-width:520px) is defined later in the file with equal
 * specificity, so it would otherwise win — leaving the overview edge-to-edge
 * and capped at 520px (#218). */
.modal.overview-dialog { max-width: min(920px, 94vw); padding: 20px 22px 16px; }
.overview-dialog-head { margin-bottom: 12px; }
.overview-body {
  max-height: 68vh;
  overflow: auto;
  /* Scroll gutter so the last floor never sits under the scrollbar. */
  padding-right: 12px;
  scrollbar-gutter: stable;
}
.floor-overview { display: flex; flex-direction: column; gap: 22px; }
.overview-floor { display: flex; flex-direction: column; gap: 6px; align-items: flex-start; max-width: 100%; }
.overview-floor-cap {
  font-family: var(--bb-font-display);
  font-size: 0.8rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--bb-ink-soft);
}
/* Scale each floor's export SVG to fit the dialog width, keeping aspect. */
.overview-canvas { max-width: 100%; }
.overview-canvas svg { max-width: 100%; height: auto; }
.overview-dialog-actions { display: flex; gap: 8px; justify-content: flex-end; margin-top: 16px; }
.overview-dialog-actions button {
  font-family: var(--bb-font-display);
  font-size: 0.78rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  padding: 7px 14px;
  background: var(--bb-parchment);
  border: 1px solid var(--bb-rule-soft);
  color: var(--bb-ink);
  cursor: pointer;
}
.overview-dialog-actions button:hover { border-color: var(--bb-gilt); }
.overview-dialog-actions .is-close { background: transparent; border-color: transparent; color: var(--bb-ink-soft); }
.overview-dialog-actions .is-close:hover { color: var(--bb-ink); border-color: var(--bb-rule-soft); }

/* Lore & Notes textarea (Charter, §5.4) — the editor that the read-mode lore
 * view swaps in. */
.notes-field {
  width: 100%;
  box-sizing: border-box;
  background: var(--bb-parchment-mid);
  border: 1px dashed var(--bb-rule-soft);
  border-radius: 0;
  padding: 10px 12px;
  resize: vertical;
  font-family: var(--bb-font-text);
  font-size: 0.9375rem;
  color: var(--bb-ink);
}
.notes-field::placeholder {
  color: var(--bb-ink-faint);
  font-family: var(--bb-font-italic);
  font-style: italic;
}
.notes-field:focus { outline: none; border-color: var(--bb-rule); }
.dialog-sub {
  font-family: var(--bb-font-italic);
  font-style: italic;
  font-size: 0.875rem;
  color: var(--bb-ink-soft);
  margin: -0.5rem 0 1rem;
}

/* Annotation editor popover (§6.5.10). Floating panel anchored next to
 * the clicked cell by JS. Parchment-light chrome with a sepia drop shadow,
 * italic input that reads as handwritten marginalia. */
.annotation-popover {
  position: fixed;
  z-index: 50;
  background: var(--bb-parchment-light);
  border: 1px solid var(--bb-rule);
  border-radius: 0;
  box-shadow: 0 10px 24px -8px var(--bb-shadow-mid);
  padding: 8px;
  min-width: 240px;
  display: flex;
  flex-direction: column;
  gap: 8px;
}

/* Kind picker (#163) reuses .type-pick/.type-pick-btn verbatim — see the
 * "Status-zone type-picker" rule further down; .note-kind-seg/.note-kind-btn
 * are scoping hooks only, no chrome of their own, matching door/stair. */

/* Size segmented control + rotate */
.annotation-meta-row {
  display: flex;
  align-items: center;
  gap: 8px;
}
.annotation-size-seg {
  display: flex;
  gap: 0;
}
.annotation-size-btn {
  padding: 2px 8px;
  font-size: 0.8125rem;
  font-family: var(--bb-font-display);
  letter-spacing: 0.05em;
  background: transparent;
  border: 1px solid var(--bb-rule);
  border-right-width: 0;
  color: var(--bb-ink-soft);
  cursor: pointer;
  border-radius: 0;
  box-shadow: none;
}
.annotation-size-btn:last-child { border-right-width: 1px; }
.annotation-size-btn:hover { background: var(--bb-parchment-mid); color: var(--bb-ink); }
.annotation-size-btn[aria-pressed="true"] {
  background: var(--bb-gilt-deep);
  border-color: var(--bb-gilt-deep);
  color: var(--bb-parchment-light);
}

/* Status-zone type-picker (#175): the on-arm segmented pick for Openings
 * (Door·Window·Arch) and Defenses (Wall·Gate), shown in the placement banner.
 * Mirrors the .annotation-size joined-plaque; selected = gilt-deep (gilt =
 * current selection within a tool; vermillion stays the ACTIVE-tool signal). */
.type-pick {
  display: flex;
  gap: 0;
  align-items: center;
}
.type-pick-btn {
  /* #218 — contain the .sr-only radio (see .theme-tile) so focusing it on
   * click can't scroll the enclosing dialog off-screen. */
  position: relative;
  padding: 4px 10px;
  font-size: 0.8125rem;
  font-family: var(--bb-font-display);
  letter-spacing: 0.05em;
  white-space: nowrap;
  background: transparent;
  border: 1px solid var(--bb-rule);
  border-right-width: 0;
  color: var(--bb-ink-soft);
  cursor: pointer;
  border-radius: 0;
  box-shadow: none;
}
.type-pick-btn:last-child { border-right-width: 1px; }
.type-pick-btn:hover { background: var(--bb-parchment-mid); color: var(--bb-ink); }
.type-pick-btn[aria-pressed="true"] {
  background: var(--bb-gilt-deep);
  border-color: var(--bb-gilt-deep);
  color: var(--bb-parchment-light);
}
/* A type segment the object can't become right now — a stair kind whose
 *  footprint won't fit at its anchor (#176 inspector). Muted + inert. */
.type-pick-btn:disabled {
  opacity: 0.4;
  cursor: not-allowed;
  color: var(--bb-ink-faint);
}
.type-pick-btn:disabled:hover { background: transparent; color: var(--bb-ink-faint); }
/* #180 Map marks — the North picker reuses the .type-pick joined-plaque chrome
 * as a native <label>-wrapped radiogroup (like the Handout palette picker in the
 * same dialog), so selected/disabled state comes from the input, not aria-pressed.
 * Scoped to the settings dialog and the Grounds Map display popover (#224);
 * reuses existing tokens, adds none. */
.settings-body .type-pick-btn:has(input:checked),
#map-display-menu .type-pick-btn:has(input:checked) {
  background: var(--bb-gilt-deep);
  border-color: var(--bb-gilt-deep);
  color: var(--bb-parchment-light);
}
/* #258 added #vtt-export-dialog: the VTT dialog gained its first disabled
 * SEGMENTS (Background/Palette gate under the data formats). #257 only ever
 * disabled the native <select>, which carries its own rule, so without this a
 * dead segment rendered fully live — and the :hover companion below matters
 * just as much, or it still lights up under the cursor. */
.settings-body .type-pick-btn:has(input:disabled),
#map-display-menu .type-pick-btn:has(input:disabled),
#vtt-export-dialog .type-pick-btn:has(input:disabled) {
  opacity: 0.4;
  cursor: not-allowed;
  color: var(--bb-ink-faint);
}
.settings-body .type-pick-btn:has(input:disabled):hover,
#map-display-menu .type-pick-btn:has(input:disabled):hover,
#vtt-export-dialog .type-pick-btn:has(input:disabled):hover { background: transparent; }
/* #215 VTT export dialog — reuse the settings radio idioms outside the settings
 * body: the .type-pick segments and .theme-tile grid take their selected state
 * from the checked radio (no JS class toggle needed). */
#vtt-export-dialog .type-pick-btn:has(input:checked) {
  background: var(--bb-gilt-deep);
  border-color: var(--bb-gilt-deep);
  color: var(--bb-parchment-light);
}
#vtt-export-dialog .theme-tile:has(input:checked) {
  border-color: var(--bb-vermillion-deep);
  box-shadow: 0 0 0 1px var(--bb-vermillion-deep);
}
#vtt-export-dialog .theme-tile-desc {
  font-family: var(--bb-font-italic);
  font-style: italic;
  font-size: 0.75rem;
  color: var(--bb-ink-soft);
}
/* The .dialog-form is a flex column with a 14px gap; the intro paragraph's 1rem
 * bottom margin stacked on top of that gap AND the first subhead's top margin,
 * so "Grid Resolution" floated in far more space than the later headings. Zero
 * the paragraph margin and trim the subhead margins so all three read evenly. */
#vtt-export-dialog .dialog-sub { margin-bottom: 0; }
#vtt-export-dialog .settings-subhead { margin-top: 0.15rem; margin-bottom: 0; }
/* Per-field caption under a VTT radiogroup (#224 Palette): disambiguates ink
 * colour from the Background opacity radio above it. Shares its register with
 * the readout's hint line (#257) — one declaration block, two callers. */
.vtt-field-note,
#vtt-export-dialog .vtt-readout-hint {
  margin: 4px 0 0;
  font-family: var(--bb-font-italic);
  font-style: italic;
  font-size: 0.8125rem;
  color: var(--bb-ink-soft);
}
#vtt-export-dialog .theme-option-group { padding-top: 8px; padding-bottom: 0; }
/* #258 — one column, still, now that there are two tiles rather than three.
 * Not the 2-col .theme-option-group default: each tile carries an "is this me?"
 * sentence that needs a full line, and at .modal's 520px a 2-col tile has a
 * ~209px text box while "Map with walls and doors" measures ~209px in the
 * display face — it wraps on the widest theme and not on others. This is also
 * the carve-out from the house "2 options = segmented .type-pick" rule
 * (Floors/Background/Palette below): .type-pick-btn is one nowrap line and
 * cannot host the desc, and this is the question that changes what the others
 * mean. Already phone-shaped, so the ≤480px block below needs nothing for it. */
#vtt-export-dialog .vtt-format-group { grid-template-columns: 1fr; }

/* #257 — three-part scaffold copied from #settings-dialog: the choices scroll,
 * the readout and the Download row stay pinned. Without it the UA scrolls the
 * whole <dialog>, carrying the title and the button off-screen; adding the
 * readout + floor select made that the common case on a phone. */
#vtt-export-dialog { overflow: hidden; }
#vtt-export-dialog .dialog-form {
  max-height: 88vh;
  max-height: 88dvh;   /* the phone URL-bar fix */
}
#vtt-export-dialog .vtt-export-body {
  min-height: 0;       /* let the flex child shrink below its content … */
  overflow-y: auto;    /* … and scroll */
  padding-right: 12px; /* house gutter so the scrollbar doesn't crowd the tiles */
  scrollbar-gutter: stable;
  display: flex;
  flex-direction: column;
  gap: 14px;           /* the .dialog-form rhythm, now that the groups are nested */
}
#vtt-export-dialog .vtt-readout,
#vtt-export-dialog .dialog-actions { flex-shrink: 0; }

/* The floor picker sits under the Floors segments; the .settings-subhead above
 * heads the group, so the select carries only an aria-label. Wrapping the three
 * into one element (so a single-floor bastion can hide the lot) makes them one
 * flex item, which drops the form's 14px rhythm between the subhead and its
 * segments — restate it. The select keeps the tighter 6px: it is a sub-control
 * of the radio above it, not the next question. */
#vtt-export-dialog #vtt-floors-group .settings-subhead { margin-bottom: 14px; }
.vtt-floor-pick { margin-top: 6px; }
#vtt-export-dialog .vtt-floor-pick select { width: 100%; }
#vtt-export-dialog .vtt-floor-pick select:disabled {
  opacity: 0.4;
  cursor: not-allowed;
}

/* The computed-result footer strip: display-face figure, colophon aside (the
 * middot register the exported sheet's own footer band speaks in), italic hint.
 * .dialog-actions carries no top border, so this hairline is the only seam and
 * reads as the ruled foot of a parchment form. */
.vtt-readout {
  margin: 0;
  padding-top: 10px;
  border-top: 1px solid var(--bb-rule);
}
.vtt-readout-figure {
  font-family: var(--bb-font-display);
  font-size: 0.9375rem;
  color: var(--bb-ink);
}
.vtt-readout-aside {
  font-size: 0.8125rem;
  color: var(--bb-ink-soft);
}
#vtt-export-dialog .vtt-readout-hint { display: block; margin-top: 2px; }
/* Same two registers as .dialog-warn (gilt) and .field-error (vermillion),
 * restated rather than cross-scoped so neither selector grows a dependency on
 * this dialog. Colour never carries the state alone — the wording changes too.
 * The `#vtt-export-dialog` prefix is load-bearing, not decoration: the hint
 * shares its base declarations with .vtt-field-note under an ID selector, so a
 * bare `.vtt-readout[data-state=…]` rule loses on specificity and the states
 * render identical ink-soft. Cascade layers don't help — same layer. */
#vtt-export-dialog .vtt-readout[data-state="warn"] .vtt-readout-hint { color: var(--bb-gilt-deep); }
#vtt-export-dialog .vtt-readout[data-state="over"] .vtt-readout-hint { color: var(--bb-vermillion); }

/* Phone tier: stack the segmented pickers. .type-pick-btn is deliberately
 * `white-space: nowrap`, so at ~292px of dialog body the Palette row measured
 * 355px and scrolled the body sideways — Floors did too, and Background only
 * escaped by a hair. Stacking gives every segment the full width on one line
 * and keeps the body's only scroll axis vertical. Segments join by suppressing
 * the right border except on the last child, so a column has to suppress the
 * bottom one instead and give every segment its right border back. Scoped to
 * this dialog: .type-pick elsewhere is short enough to stay inline. */
@media (max-width: 480px) {
  #vtt-export-dialog .vtt-radio-seg { flex-direction: column; align-items: stretch; }
  #vtt-export-dialog .vtt-radio-seg .type-pick-btn {
    border-right-width: 1px;
    border-bottom-width: 0;
    text-align: left;
  }
  #vtt-export-dialog .vtt-radio-seg .type-pick-btn:last-child { border-bottom-width: 1px; }
  #handout-dialog .vtt-radio-seg { flex-direction: column; align-items: stretch; }
  #handout-dialog .vtt-radio-seg .type-pick-btn {
    border-right-width: 1px;
    border-bottom-width: 0;
    text-align: left;
  }
  #handout-dialog .vtt-radio-seg .type-pick-btn:last-child { border-bottom-width: 1px; }
}

/* ---- Open handout dialog (#292) ----------------------------------------- */
/* The include-set that used to be a Settings group. It reuses Settings' own row
 * and toggle vocabulary rather than inventing one, which is why the body
 * carries `.settings-body`: that class is the hook in all seven `.view-toggle`
 * selector lists above, so a bare `#handout-dialog .view-toggle` would have
 * rendered as an unstyled native checkbox. Only the frame is new here, and it
 * mirrors #settings-dialog's — body scrolls, actions stay pinned. */
#handout-dialog { overflow: hidden; }
#handout-dialog .dialog-form {
  max-height: 88vh;
  max-height: 88dvh;
}
#handout-dialog .dialog-sub { margin-bottom: 0; }
#handout-dialog .settings-body {
  margin-top: 0;
  min-height: 0;
  overflow-y: auto;
  padding-right: 12px;   /* house gutter so the scrollbar doesn't crowd toggles */
  scrollbar-gutter: stable;
}
#handout-dialog .settings-subhead { margin-top: 0; margin-bottom: 0.25rem; }
#handout-dialog .settings-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  column-gap: 10px;
  row-gap: 2px;
  align-items: center;
  border-bottom: none;
  padding: 4px 0;
}
#handout-dialog .settings-row .settings-sub {
  grid-column: 1 / -1;
  padding-left: 26px;    /* hangs the description under the label, clear of the box */
}
/* The row IS the tap target, at every width — a 18px box is a poor one on a
 * phone and no worse a target on a desktop for being wide. Doing it unscoped
 * avoids a phone-tier rule, which #283 would otherwise oblige this to be. */
#handout-dialog .view-toggle { display: flex; width: 100%; }
/* Map key is a child of Floor plans, not its sibling: it keys a plate, and
 * without one it has nothing to number. Disabled in place rather than hidden —
 * a control that vanishes mid-dialog reflows the list under the pointer. */
#handout-dialog .settings-row.is-child { padding-left: 26px; }
#handout-dialog .settings-row.is-disabled { opacity: 0.45; }
#handout-dialog .settings-row.is-disabled .view-toggle { cursor: not-allowed; }
/* The last surviving tick of {plans, orders, register} can't be cleared — a
 * handout with none of the three is not a sparser handout, it is a cover sheet.
 * Said in words below the list rather than in a `data-tip`, which would render
 * behind the dialog's top layer and so never be read at all. */
.handout-guard-note {
  margin: 6px 0 0;
  font-family: var(--bb-font-italic);
  font-style: italic;
  font-size: 0.8125rem;
  color: var(--bb-gilt-deep);
}
.handout-guard-note[hidden] { display: none; }

/* The ≤900px per-mode banner wrap (#176/#237) is GONE (#275). It dropped the
 * Stairs and Defenses pickers onto a second banner line, which broke the command
 * rail's own promise — arming a tool changed the rail's height and shifted the
 * canvas, the one thing a constant-height status zone exists to prevent. It was
 * only ever needed because the banner was trapped in the canvas column at 424px;
 * the rail now spans the chapter, so the pickers share the row at every width.
 * The replacement is structural and mode-independent, in the ≤1024px Grounds
 * tier: `flex-wrap: nowrap` + a non-shrinking `.type-pick`. */
/* Persistent ▾ on a sub-typed toolbar tool (#175 discoverability backstop) —
 * currentColor so it stays legible on the vermillion active button. */
.mode-btn .mode-caret {
  opacity: 0.55;
  font-size: 0.7em;
  margin-left: 2px;
}
.annotation-rotate-btn {
  padding: 2px 6px;
  background: transparent;
  border: 1px solid var(--bb-rule);
  color: var(--bb-ink-soft);
  cursor: pointer;
  border-radius: 0;
  box-shadow: none;
  display: flex;
  align-items: center;
}
.annotation-rotate-btn:hover { background: var(--bb-parchment-mid); color: var(--bb-ink); }
.annotation-rotate-btn[aria-pressed="true"] {
  background: var(--bb-gilt-deep);
  border-color: var(--bb-gilt-deep);
  color: var(--bb-parchment-light);
}
.annotation-rotate-btn .glyph-svg {
  width: 14px;
  height: 14px;
  display: block;
}

/* Textarea */
.annotation-popover textarea {
  background: transparent;
  border: none;
  border-radius: 0;
  padding: 4px 6px;
  color: var(--bb-ink);
  font-family: var(--bb-font-italic);
  font-style: italic;
  font-size: 0.9375rem;
  width: 100%;
  box-sizing: border-box;
  box-shadow: none;
  resize: none;
  field-sizing: content;
  min-height: 2.5em;
}
.annotation-popover textarea:focus {
  outline: none;
  background: var(--bb-parchment-mid);
}

/* Legend helper line (#163) — shown only when the Legend kind is selected.
 * Names what the marker becomes; the #181 Map-key strip below is the actual
 * tether. Deliberately not .dialog-warn (that's dialog-scoped, reads as a
 * caution, not a fact). */
.annotation-legend-hint {
  margin: 0;
  font-family: var(--bb-font-italic);
  font-style: italic;
  font-size: 0.8rem;
  color: var(--bb-ink-soft);
}

/* Map-key strip + keyed-marker ↔ legend tether (#181) — the in-app legend
 * beneath each floor. Numbering is shared with the canvas discs + handout/SVG
 * key (keyedAnnotationsForFloor). No new tokens: the badge + rail-bar are an
 * identity echo of the (already vermillion, #163) disc, NOT a new
 * "vermillion = state" use, so they stay vermillion, not gilt. Every tether
 * state pairs hue with a shape cue (rail-bar / ring / badge) → not color-only. */
.map-key-strip {
  margin-top: 8px;
  max-width: 100%;
}
.map-key-strip-head {
  font-family: var(--bb-font-display);
  font-size: 0.6875rem;              /* 11px */
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--bb-ink-faint);
  margin-bottom: 4px;
}
.map-key-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 2px;
}
.map-key-row {
  display: flex;
  align-items: baseline;
  gap: 8px;
  width: 100%;
  padding: 3px 6px;
  border: none;
  border-radius: 0;
  background: transparent;
  box-shadow: inset 3px 0 0 transparent;
  text-align: left;
  line-height: 1.5;
  cursor: pointer;
  transition: background-color 0.12s, box-shadow 0.12s;
}
/* Rest → linked/hover/active share the rail-bar + parchment wash. The active
 * row (aria-current, set while its editor popover is open) carries .is-linked
 * persistently; the attribute is the SR differentiator, not extra colour. */
.map-key-row:hover,
.map-key-row.is-linked {
  background: var(--bb-parchment-mid);
  box-shadow: inset 3px 0 0 var(--bb-vermillion);
}
.map-key-badge {
  flex: 0 0 auto;
  align-self: center;
  width: 18px;
  height: 18px;
  border-radius: 50%;
  background: var(--bb-vermillion);
  border: 1.5px solid var(--bb-vermillion-deep);
  color: #f8efd4;                    /* the disc's literal parchment numeral */
  font-family: var(--bb-font-display);
  font-weight: 500;
  font-size: 11px;
  font-variant-numeric: lining-nums tabular-nums;
  display: flex;
  align-items: center;
  justify-content: center;
}
.map-key-row .label {
  font-family: var(--bb-font-italic);
  font-style: italic;
  color: var(--bb-ink-soft);
  white-space: normal;               /* wrap, never truncate (annotation convention) */
  overflow-wrap: anywhere;
}
/* Tether echo on the canvas disc: a static ring reusing the :focus shape. */
.annotation-keyed.is-linked {
  outline: 2px solid var(--bb-vermillion);
  outline-offset: 2px;
}
/* One-shot reveal glow when a map-key row is clicked — reuses the #89 SVG
 * drop-shadow mechanism (vermillion here, an identity echo — not the temporal
 * gilt highlight). Motion-gated; under reduced motion the scroll carries it. */
@media (prefers-reduced-motion: no-preference) {
  .annotation-keyed.is-revealing {
    animation: map-key-reveal 1.2s ease-out forwards;
  }
}
@keyframes map-key-reveal {
  0% {
    filter: drop-shadow(0 0 3px var(--bb-vermillion))
            drop-shadow(0 0 7px var(--bb-vermillion));
  }
  100% {
    filter: drop-shadow(0 0 0 transparent);
  }
}

.annotation-popover-actions {
  display: flex;
  gap: 6px;
  justify-content: flex-end;
}
.annotation-popover-actions button {
  padding: 4px 12px;
  font-family: var(--bb-font-display);
  font-size: 0.8125rem;              /* 13px */
  letter-spacing: 0.10em;
  text-transform: uppercase;
}

.charter-identity .roster { display: flex; flex-direction: column; gap: 14px; }
.charter-identity .char-focus-btn {
  background: none;
  border: none;
  padding: 0;
  cursor: pointer;
  font-family: var(--bb-font-italic);
  font-style: italic;
  font-size: 0.9375rem;
  color: var(--bb-vermillion);
  text-decoration: underline dotted;
  text-underline-offset: 2px;
}
.charter-identity .char-focus-btn:hover { color: var(--bb-vermillion-deep); }
.charter-identity .char-focus-btn.active { color: var(--bb-ink-soft); }
.charter-identity .char-focus-btn.active:hover { color: var(--bb-ink); }
.charter-identity .char-focus-btn.btn-add-char { color: var(--bb-ink-soft); }
.charter-identity .char-focus-btn.btn-add-char:hover { color: var(--bb-ink); }
.charter-identity .char-focus-btn.btn-remove-char { color: var(--bb-danger); }
.charter-identity .char-focus-btn.btn-remove-char:hover { color: var(--bb-danger-deep); }

/* ---- Owner cards (#147) ------------------------------------------------- */

.charter-owners {
  margin-top: 18px;
  padding-top: 0;
  border-top: none;
}
.owners-label { margin-bottom: 10px; }
.party-name-field { margin-bottom: 14px; }
.party-name {
  font-family: var(--bb-font-italic);
  font-style: italic;
  font-size: 0.875rem;
  background: transparent;
  border: 1px solid transparent;
  color: var(--bb-ink);
  padding: 2px 6px;
  box-shadow: none;
  border-radius: 0;
  width: 100%;
}
.party-name:hover {
  background: var(--bb-parchment-mid);
  border-color: var(--bb-rule-soft);
}
.party-name:focus {
  background: var(--bb-parchment-light);
  border-color: var(--bb-vermillion);
  outline: none;
}
.owner-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: 14px;
}
.owner-card {
  background: var(--bb-parchment-light);
  border: 1px solid var(--bb-rule);
  padding: 14px;
  display: flex;
  flex-direction: column;
  gap: 12px;
  position: relative;
}
/* "+ Add owner" tile — dashed ghost card */
button.owner-card.owner-add {
  background: none;
  border: 1px dashed var(--bb-rule-soft);
  color: var(--bb-ink-soft);
  font-family: var(--bb-font-italic);
  font-style: italic;
  font-size: 0.9375rem;
  cursor: pointer;
  min-height: 72px;
  align-self: start;
  display: flex;
  align-items: center;
  justify-content: center;
}
button.owner-card.owner-add:hover {
  border-color: var(--bb-vermillion);
  color: var(--bb-vermillion);
}
/* Owner head: portrait + id column side by side */
.owner-head {
  display: flex;
  gap: 12px;
  align-items: flex-start;
}
.owner-portrait-wrap {
  position: relative;
  flex-shrink: 0;
}
/* Square ~72px portrait miniature — mirrors .crest-slot */
.owner-portrait {
  display: block;
  width: 72px;
  height: 72px;
  padding: 2px;
  background: var(--bb-parchment);
  border: 1px solid var(--bb-rule-soft);
  border-radius: 0;
  cursor: pointer;
  box-shadow: none;
  overflow: hidden;
}
.owner-portrait-img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}
/* No portrait uploaded (#272): a monogram in the display face, or — with no
   name to abbreviate — bare parchment inside the dashed slot. What this replaced
   was a line-art user icon baked into a data-URI: line art and a modern object,
   both against the icon vocabulary, and a reader of a shared snapshot met a row
   of them. */
.owner-portrait-monogram,
.owner-portrait-blank {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  height: 100%;
  background: var(--bb-parchment-mid);
}
.owner-portrait-monogram {
  font-family: var(--bb-font-display);
  font-size: 2.2rem;
  line-height: 1;
  color: var(--bb-ink-faint);
  user-select: none;
}
.owner-portrait.is-placeholder {
  border-style: dashed;
  border-color: var(--bb-rule-soft);
}
.owner-portrait:not(.is-placeholder):hover,
.owner-portrait:focus-visible {
  outline: 2px solid var(--bb-vermillion);
  outline-offset: 2px;
}
.owner-portrait.is-placeholder:hover,
.owner-portrait.is-placeholder:focus-visible {
  border-color: var(--bb-vermillion);
  outline: none;
}
/* Portrait upload/replace/remove menu — opens below the portrait */
.owner-portrait-menu {
  position: absolute;
  top: calc(100% + 4px);
  left: 0;
  z-index: 200;
  min-width: 160px;
}
.owner-id {
  flex: 1 1 0;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 6px;
}
.owner-id .char-name {
  font-family: var(--bb-font-display);
  font-size: 1rem;
  font-weight: 600;
  color: var(--bb-ink);
  background: transparent;
  border: 1px solid transparent;
  padding: 2px 4px;
  box-shadow: none;
  border-radius: 0;
  width: 100%;
}
.owner-id .char-name:hover {
  background: var(--bb-parchment-mid);
  border-color: var(--bb-rule-soft);
}
.owner-id .char-name:focus {
  background: var(--bb-parchment-light);
  border-color: var(--bb-vermillion);
  outline: none;
}
.owner-meta {
  display: flex;
  align-items: baseline;
  gap: 10px;
  flex-wrap: wrap;
}
.owner-level-label {
  font-family: var(--bb-font-italic);
  font-style: italic;
  font-size: 0.875rem;
  color: var(--bb-ink-soft);
  display: flex;
  align-items: baseline;
  gap: 4px;
  white-space: nowrap;
}
.owner-level-label .lvl {
  width: 2em;
  min-width: 0;
  color: var(--bb-vermillion-deep);
  font-family: var(--bb-font-display);
  font-style: normal;
  font-weight: 600;
  font-variant-numeric: lining-nums tabular-nums;
  text-align: center;
  background: transparent;
  border: 1px solid transparent;
  padding: 1px 2px;
  box-shadow: none;
  border-radius: 0;
  -moz-appearance: textfield;
}
.owner-level-label .lvl::-webkit-outer-spin-button,
.owner-level-label .lvl::-webkit-inner-spin-button {
  -webkit-appearance: none;
  margin: 0;
}
.owner-level-label .lvl:hover {
  background: var(--bb-parchment-mid);
  border-color: var(--bb-rule-soft);
}
.owner-level-label .lvl:focus {
  background: var(--bb-parchment-light);
  border-color: var(--bb-vermillion);
  outline: none;
}
.owner-class {
  font-family: var(--bb-font-italic);
  font-style: italic;
  font-size: 0.84rem;
  color: var(--bb-ink-soft);
  background: transparent;
  border: 1px solid transparent;
  padding: 1px 4px;
  box-shadow: none;
  border-radius: 0;
  min-width: 0;
  flex: 1 1 0;
  box-sizing: border-box;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.owner-class:hover {
  background: var(--bb-parchment-mid);
  border-color: var(--bb-rule-soft);
}
.owner-class:focus {
  background: var(--bb-parchment-light);
  border-color: var(--bb-gilt-deep);
  outline: none;
}
.owner-class::placeholder { color: var(--bb-ink-faint); font-style: italic; }
/* Owner standing: credential chip groups */
.owner-standing {
  display: flex;
  flex-direction: column;
  gap: 8px;
  position: relative;
}
.cred-group { display: flex; flex-direction: column; gap: 4px; }
.cred-group-label {
  font-family: var(--bb-font-display);
  font-size: 0.75rem;
  color: var(--bb-ink-faint);
  text-transform: uppercase;
  letter-spacing: 0.04em;
}
.cred-chips {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
}
/* Base chip — parchment-mid background; vermillion/sapphire stay for facility kinds */
.cred-chip {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 2px 8px;
  background: var(--bb-parchment-mid);
  border: 1px solid var(--bb-rule-soft);
  font-family: var(--bb-font-text);
  font-size: 0.8125rem;
  color: var(--bb-ink-soft);
  cursor: default;
  white-space: nowrap;
}
/* Dashed "+" add-chip — resets to a clickable ghost */
button.cred-chip.cred-add {
  background: none;
  border-style: dashed;
  border-color: var(--bb-rule-soft);
  color: var(--bb-ink-faint);
  cursor: pointer;
  align-self: flex-start;
}
button.cred-chip.cred-add:hover {
  border-color: var(--bb-vermillion);
  color: var(--bb-vermillion);
}
/* Renown chip carries a score input inline */
.cred-chip-renown { padding: 2px 6px; }
.cred-renown-house {
  font-family: var(--bb-font-text);
  font-size: 0.8125rem;
  color: var(--bb-ink-soft);
}
/* The score reads as inline editable illumination on the chip's parchment,
 * not a form field dropped into it: no box, no spinners — just a hairline
 * underline (#218). */
.cred-renown-score {
  width: 34px;
  /* Mandatory, not tidiness: the global `input, select { min-width: 120px }`
     field floor beats any `width` — min-width always wins, and specificity is
     irrelevant — so this field rendered at 120px, wide enough for 18 digits on
     a score the markup caps at 30, and every renown chip was 234px because of
     it. Same trap the Settings and turn-page fields carry a note about. */
  min-width: 0;
  padding: 0 2px;
  appearance: none;
  -webkit-appearance: none;
  -moz-appearance: textfield;
  background: transparent;
  border: 0;
  border-bottom: 1px solid var(--bb-rule-soft);
  border-radius: 0;
  color: var(--bb-ink);
  font-family: var(--bb-font-text);
  font-size: 0.8125rem;
  font-variant-numeric: lining-nums tabular-nums;
  text-align: right;
  line-height: 1.4;
}
.cred-renown-score::-webkit-inner-spin-button,
.cred-renown-score::-webkit-outer-spin-button {
  -webkit-appearance: none;
  margin: 0;
}
.cred-renown-score:focus {
  outline: 2px solid var(--bb-gilt-deep);
  outline-offset: 1px;
}
/* × remove button inside a chip */
.cred-chip-remove {
  background: none;
  border: none;
  padding: 0;
  color: var(--bb-ink-faint);
  cursor: pointer;
  font-size: 1rem;
  line-height: 1;
  margin-left: 2px;
}
.cred-chip-remove:hover { color: var(--bb-vermillion); }
.cred-chip-remove:focus-visible { outline: 2px solid var(--bb-vermillion); outline-offset: 1px; }
/* Add-credential dropdown menu */
.cred-add-menu {
  position: absolute;
  z-index: 200;
  background: var(--bb-parchment-light);
  border: 1px solid var(--bb-rule);
  box-shadow: var(--shadow-pop);
  min-width: 200px;
  max-width: 280px;
}
.cred-menu-group + .cred-menu-group {
  border-top: 1px solid var(--bb-rule-soft);
}
.cred-menu-group-label {
  font-family: var(--bb-font-display);
  font-size: 0.75rem;
  color: var(--bb-ink-faint);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  padding: 6px 12px 2px;
}
.cred-menu-row {
  display: block;
  width: 100%;
  text-align: left;
  background: none;
  border: none;
  padding: 5px 12px;
  font-family: var(--bb-font-text);
  font-size: 0.84rem;
  color: var(--bb-ink-soft);
  cursor: pointer;
}
.cred-menu-row:hover,
.cred-menu-row:focus-visible { background: var(--bb-parchment-mid); color: var(--bb-ink); outline: none; }
.cred-menu-empty {
  padding: 8px 12px;
  font-family: var(--bb-font-italic);
  font-style: italic;
  font-size: 0.84rem;
  color: var(--bb-ink-faint);
}
/* Owner card footer */
.owner-foot {
  display: flex;
  justify-content: space-between;
  align-items: center;
  min-height: 1.4em;   /* reserve space even when empty (single owner) */
}
.owner-foot-btn {
  background: none;
  border: none;
  padding: 0;
  font-family: var(--bb-font-italic);
  font-style: italic;
  font-size: 0.875rem;
  cursor: pointer;
  color: var(--bb-danger);
  text-decoration: underline dotted;
  text-underline-offset: 2px;
}
.owner-foot-btn:hover { color: var(--bb-danger-deep); }
/* Import button — same italic-link idiom as remove, non-danger color */
.owner-import-btn { color: var(--bb-ink-soft); }
.owner-import-btn:hover { color: var(--bb-vermillion); }
/* Picker hint → jump link. A <button> in link clothing: the global `button`
   chrome (gradient, border, box-shadow, :hover gradient, :active nudge) must
   be beaten per-state — `button:hover` outspecifies a lone class, so the
   hover/active resets below are load-bearing, not belt-and-braces. */
.picker-hint-link {
  background: none;
  border: none;
  box-shadow: none;
  padding: 0;
  font: inherit;
  color: var(--bb-vermillion);
  text-decoration: underline dotted;
  text-underline-offset: 2px;
  cursor: pointer;
}
.picker-hint-link:hover {
  background: none;
  border: none;
  color: var(--bb-vermillion-deep);
}
.picker-hint-link:active { transform: none; }
/* Owner card highlight — vermillion pulse on picker-hint jump */
@keyframes owner-highlight-pulse {
  0%   { outline-color: var(--bb-vermillion); outline-offset: 0px; }
  50%  { outline-color: var(--bb-vermillion); outline-offset: 3px; }
  100% { outline-color: transparent; outline-offset: 3px; }
}
.owner-card.is-highlight {
  outline: 2px solid var(--bb-vermillion);
}
@media (prefers-reduced-motion: no-preference) {
  .owner-card.is-highlight {
    animation: owner-highlight-pulse 1.2s ease-out forwards;
  }
}
/* Status toast (Codex §7.3) — floats at the bottom of the viewport with
 * a parchment-light gradient and per-status border tints. */
.status-toast {
  position: fixed;
  bottom: 24px;
  left: 50%;
  transform: translateX(-50%);
  min-width: 160px;
  max-width: 80vw;
  padding: 10px 18px;
  border-radius: 0;
  background: linear-gradient(180deg, var(--bb-parchment-light), var(--bb-parchment-mid));
  color: var(--bb-ink);
  font-family: var(--bb-font-display);
  font-size: 0.875rem;              /* 14px */
  border: 1px solid var(--bb-rule);
  box-shadow: 0 8px 24px -8px var(--bb-shadow-deep);
  z-index: 50;
  text-align: center;
  pointer-events: none;
}
.status-toast:empty { display: none; }
.status-toast.success { border-color: var(--bb-verdigris); }
.status-toast.error   { border-color: var(--bb-danger); }
.status-toast.warn    { border-color: var(--bb-gilt); }

/* ---- Themed confirm + choice dialog (Codex §7.1) ------------------------ */

} /* @layer components — pause: the base modal shell belongs in `base` (#259) */

/* The documented footgun (#259): this default — padding:0; max-width:520px —
   sat physically late in the file and clobbered earlier per-dialog overrides
   purely by source order (the `.modal.overview-dialog` case). In the `base`
   layer it is beaten by any `.modal.<variant>` in components/themes by layer,
   so a single-class dialog override no longer needs the `.modal.<x>` compound-
   specificity dodge to win. */
@layer base {
.modal {
  border: 1px solid var(--bb-rule);
  border-radius: 0;
  background: linear-gradient(180deg, var(--bb-parchment-light), var(--bb-parchment-mid));
  color: var(--bb-ink);
  padding: 0;
  max-width: 520px;
  width: 90vw;
  box-shadow: 0 16px 36px -10px var(--bb-shadow-deep);
}
.modal::backdrop {
  background: var(--bb-backdrop);
  backdrop-filter: blur(2px);
}
}

@layer components {

.dialog-form {
  padding: 22px 24px 18px;
  display: flex;
  flex-direction: column;
  gap: 14px;
}
.dialog-title {
  margin: 0;
  font-family: var(--bb-font-display);
  font-size: 1.125rem;              /* 18px */
  color: var(--bb-vermillion-deep);
  letter-spacing: 0.01em;
}
.dialog-body {
  margin: 0;
  white-space: pre-line;
  color: var(--bb-ink-soft);
  font-family: var(--bb-font-text);
  font-size: 0.9375rem;             /* 15px */
  line-height: 1.45;
  max-height: 60vh;
  overflow-y: auto;
}
/* Text-input body for prompts that collect a value rather than just a
   decision (#195 off-map location note) — same recipe as .founding-input. */
.dialog-field-label {
  display: block;
  margin-bottom: 6px;
  font-family: var(--bb-font-display);
  font-size: 0.8125rem;
  color: var(--bb-ink-soft);
}
.dialog-text-input {
  width: 100%;
  box-sizing: border-box;
  font-family: var(--bb-font-text);
  font-size: 0.9375rem;
  color: var(--bb-ink);
  background: var(--bb-parchment-light);
  border: 1px solid var(--bb-rule);
  border-radius: 0;
  padding: 7px 9px;
}
.dialog-text-input::placeholder {
  color: var(--bb-ink-faint);
  font-family: var(--bb-font-italic);
  font-style: italic;
}
.dialog-text-input:focus {
  outline: none;
  border-color: var(--bb-vermillion);
  background: var(--bb-parchment-light);
}

.dialog-actions {
  display: flex;
  flex-wrap: wrap;
  justify-content: flex-end;
  gap: 8px;
  margin-top: 4px;
}
.dialog-actions button { min-width: 80px; padding-inline: 10px; }
.dialog-actions-break { flex-basis: 100%; height: 0; }

/* Cancel = ghost text, Confirm = gilt (additive) or vermillion (danger).
   Also reused (via .picker) for the Commission dialog's own Cancel — a
   dismiss action, same tier as Done. Wizard-nav Back uses .picker-back
   instead (see below), a lighter text-link so it doesn't read as a peer
   of Cancel/Done. */
.dialog-actions .dialog-cancel,
.picker .dialog-cancel {
  background: transparent;
  border: 1px solid transparent;
  color: var(--bb-ink-soft);
  box-shadow: none;
  font-family: var(--bb-font-display);
  font-size: 0.75rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
}
.dialog-actions .dialog-cancel:hover,
.picker .dialog-cancel:hover {
  background: var(--bb-parchment-mid);
  color: var(--bb-ink);
  border-color: var(--bb-rule-soft);
}
.picker-back {
  background: transparent;
  border: none;
  box-shadow: none;
  color: var(--bb-ink-soft);
  font-family: var(--bb-font-italic);
  font-style: italic;
  font-size: 0.8125rem;
  padding: 4px 2px;
  cursor: pointer;
  text-decoration: underline;
  text-decoration-color: transparent;
  transition: color 0.12s, text-decoration-color 0.12s;
}
.picker-back:hover { color: var(--bb-ink); text-decoration-color: var(--bb-ink-soft); }
.picker-back:focus-visible { outline: 2px solid var(--bb-gilt-deep); outline-offset: 2px; }
.dialog-actions .dialog-confirm {
  background: linear-gradient(180deg, var(--bb-gilt-bright), var(--bb-gilt));
  color: var(--bb-ink);
  border: 1px solid var(--bb-gilt-deep);
  box-shadow:
    0 1px 0 var(--bb-inset-strong) inset,
    0 2px 4px var(--bb-shadow);
  font-family: var(--bb-font-display);
  font-size: 0.75rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
}
.dialog-actions .dialog-confirm:hover {
  background: linear-gradient(180deg,
      color-mix(in oklch, var(--bb-gilt-bright), white 6%),
      var(--bb-gilt-bright));
  border-color: var(--bb-gilt-deep);
}
.dialog-actions .dialog-confirm.danger {
  background: var(--bb-danger);
  color: var(--bb-parchment-light);
  border-color: var(--bb-danger-deep);
}
.dialog-actions .dialog-confirm.danger:hover {
  background: var(--bb-danger-deep);
  border-color: var(--bb-danger-deep);
}
.dialog-actions .dialog-confirm:focus-visible {
  outline: 2px solid var(--bb-vermillion);
  outline-offset: 2px;
}

/* List-layout choice tiles (opt-in via choiceModal layout:"list", §7.3).
 * Stacks full-width tiles; primary = gilt, rest = ghost. Reuses the
 * gilt/ghost hierarchy already established by .dialog-confirm/.dialog-cancel. */
.dialog-actions.dialog-actions-list {
  flex-direction: column;
  align-items: stretch;
  justify-content: flex-start;
}
.dialog-actions-list .dialog-choice {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 2px 10px;
  min-width: 0;
  min-height: 3.4rem;
  padding: 10px 14px;
  text-align: left;
  background: var(--bb-parchment-mid);
  border: 1px solid var(--bb-rule-soft);
  color: var(--bb-ink);
}
.dialog-actions-list .dialog-choice:hover {
  background: var(--bb-parchment-light);
  border-color: var(--bb-rule);
}
.dialog-actions-list .dialog-choice.primary {
  background: linear-gradient(180deg, var(--bb-gilt-bright), var(--bb-gilt));
  border: 1px solid var(--bb-gilt-deep);
  box-shadow:
    0 1px 0 var(--bb-inset-strong) inset,
    0 2px 4px var(--bb-shadow);
}
.dialog-actions-list .dialog-choice.primary:hover {
  background: linear-gradient(180deg,
    color-mix(in oklch, var(--bb-gilt-bright), white 6%),
    var(--bb-gilt-bright));
}
.dialog-actions-list .dialog-choice-label {
  flex: 1 1 auto;
  font-family: var(--bb-font-display);
  font-size: 0.75rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
}
.dialog-actions-list .dialog-choice-hint {
  flex-basis: 100%;
  font-family: var(--bb-font-italic);
  font-style: italic;
  font-size: 0.875rem;
  color: var(--bb-ink-soft);
}
.dialog-actions-list .dialog-choice.warn {
  border-left: 3px solid var(--bb-gilt-deep);
}
.dialog-actions-list .dialog-choice.warn .dialog-choice-hint {
  color: var(--bb-gilt-deep);
}
.dialog-actions-list .dialog-choice.danger {
  background: var(--bb-danger);
  border-color: var(--bb-danger-deep);
  color: var(--bb-parchment-light);
}
.dialog-actions-list .dialog-choice.danger .dialog-choice-hint {
  color: var(--bb-parchment-mid);
}
.dialog-actions-list .dialog-choice:focus-visible {
  outline: 2px solid var(--bb-vermillion);
  outline-offset: 2px;
}
.dialog-actions-list .dialog-cancel {
  align-self: flex-end;
  margin-top: 2px;
}


/* ---- Founding flow + welcome view --------------------------------------- */
/* Founding form (#159) — the "Create your Bastion" view swapped into
   #welcome-dialog. Reuses .dialog-*, .view-toggle, .settings-row, .lvl and the
   .charter-sources disclosure recipe; only the form's own layout lives here.
   The rarely-touched sections (type / sourcebooks / focuses) collapse so the
   resting form stays short enough not to scroll. */
.welcome-view[hidden] { display: none; }
/* The welcome views are block-wrapped (not the six-dialog flex-gap layout), so
   .dialog-sub's -0.5rem top margin pulls it into the title — cancel it here. */
#welcome-dialog .dialog-sub { margin-top: 0; }
.founding-body {
  display: flex;
  flex-direction: column;
  gap: 13px;
  white-space: normal;                 /* structured markup — undo the modal-body pre-line */
  scrollbar-gutter: stable both-edges; /* symmetric gutter; no lopsided right margin */
}
.founding-field {
  display: flex;
  flex-direction: column;
  gap: 5px;
  margin: 0;
  border: none;
  padding: 0;
}
.founding-label {
  font-family: var(--bb-font-display);
  font-size: 0.75rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--bb-ink-soft);
}
.founding-optional {
  font-family: var(--bb-font-italic);
  font-style: italic;
  letter-spacing: 0;
  text-transform: none;
  color: var(--bb-ink-faint);
}
.founding-input {
  font-family: var(--bb-font-text);
  font-size: 0.9375rem;
  color: var(--bb-ink);
  background: var(--bb-parchment-light);
  border: 1px solid var(--bb-rule);
  border-radius: 0;
  padding: 7px 9px;
}
.founding-input::placeholder {
  color: var(--bb-ink-faint);
  font-family: var(--bb-font-italic);
  font-style: italic;
}
.founding-input:focus {
  outline: none;
  border-color: var(--bb-vermillion);
  background: var(--bb-parchment-light);
}

/* Level stepper + teaching line, side by side to save vertical space. */
.founding-level-row {
  display: flex;
  align-items: baseline;
  gap: 12px;
}
.founding-level-field .lvl {
  width: 3.2em;
  min-width: 0;
  color: var(--bb-vermillion-deep);
  font-family: var(--bb-font-display);
  font-weight: 600;
  font-variant-numeric: lining-nums tabular-nums;
  text-align: center;
  background: var(--bb-parchment-light);
  border: 1px solid var(--bb-rule);
  padding: 5px 4px;
}
.founding-level-field .lvl:focus {
  outline: none;
  border-color: var(--bb-vermillion);
}
.founding-threshold {
  margin: 0;
  font-size: 0.875rem;
  color: var(--bb-ink-soft);
}
.founding-threshold.is-locked {
  font-family: var(--bb-font-italic);
  font-style: italic;
  color: var(--bb-gilt-deep);
}

/* Import from D&D Beyond — a prominent secondary path (parchment, bordered),
   quieter than the gilt Create button but clearly featured. */
.founding-ddb {
  display: flex;
  flex-direction: column;
  gap: 2px;
  width: 100%;
  text-align: left;
  padding: 10px 14px;
  background: var(--bb-parchment-mid);
  border: 1px solid var(--bb-rule);
  color: var(--bb-ink);
  cursor: pointer;
}
.founding-ddb:hover {
  background: var(--bb-parchment-light);
  border-color: var(--bb-vermillion);
}
.founding-ddb:focus-visible {
  outline: 2px solid var(--bb-vermillion);
  outline-offset: 2px;
}
.founding-ddb-label {
  font-family: var(--bb-font-display);
  font-size: 0.78rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--bb-vermillion-deep);
}
.founding-ddb-hint {
  font-family: var(--bb-font-italic);
  font-style: italic;
  font-size: 0.8125rem;
  color: var(--bb-ink-soft);
}

/* Collapsed sections — the .charter-sources disclosure chrome (title + right
   summary + chevron), reused so type / sourcebooks / focuses default closed. */
.founding-collapse {
  border-top: 1px solid var(--bb-rule-soft);
}
.founding-collapse > summary {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 9px 0;
  cursor: pointer;
  list-style: none;
}
.founding-collapse > summary::-webkit-details-marker { display: none; }
.founding-collapse-title {
  margin-right: auto;   /* pushes the summary value + chevron to the far right */
  font-family: var(--bb-font-display);
  font-size: 0.75rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--bb-ink-soft);
}
.founding-collapse-summary {
  font-family: var(--bb-font-italic);
  font-style: italic;
  font-size: 0.8125rem;
  color: var(--bb-ink-soft);
}
.founding-collapse > summary::after {
  content: "";
  flex: none;
  width: 7px; height: 7px;
  margin-right: 3px;
  border-right: 1.5px solid var(--bb-ink-soft);
  border-bottom: 1.5px solid var(--bb-ink-soft);
  transform: rotate(45deg);
  transition: transform 120ms;
}
.founding-collapse[open] > summary::after { transform: rotate(-135deg); }
.founding-collapse > summary:hover .founding-collapse-title,
.founding-collapse > summary:focus-visible .founding-collapse-title { color: var(--bb-vermillion); }
.founding-collapse > summary:hover::after,
.founding-collapse > summary:focus-visible::after { border-color: var(--bb-vermillion); }
.founding-collapse > summary:focus-visible {
  outline: 2px solid var(--bb-vermillion);
  outline-offset: 2px;
}
.founding-collapse[open] { padding-bottom: 8px; }
.founding-collapse .chapter-sub { margin: 0 0 8px; }
/* Toggle rows inside a collapsed section — no trailing rule under the last one. */
.founding-collapse .settings-row:last-of-type { border-bottom: none; }
.founding-collapse .settings-row .view-toggle { flex: none; }

.founding-type-hint {
  margin: 6px 0 0;
  font-family: var(--bb-font-italic);
  font-style: italic;
  font-size: 0.8125rem;
  color: var(--bb-gilt-deep);
}
.founding-type-hint strong { color: var(--bb-gilt-deep); font-style: normal; }
/* Mobile rows dim when Eberron isn't enabled for this bastion. */
.founding-type-mobile:has(input:disabled) {
  opacity: 0.5;
}
.founding-focus-options {
  display: flex;
  flex-direction: column;
  gap: 8px;
}


/* ---- Milestones: lines, ids, footer ------------------------------------- */
/* Upgrade-confirm ledger (§7.2) — mono nums per step row, hairline rule
 * above the total. */
.dialog-body .upgrade-lead {
  margin: 0;
}
.dialog-body .upgrade-ledger {
  list-style: none;
  margin: 12px 0 0;
  padding: 0;
  font-family: var(--bb-font-mono);
  font-size: 0.8125rem;               /* 13px */
  font-variant-numeric: lining-nums tabular-nums;
}
.dialog-body .upgrade-ledger li {
  display: grid;
  grid-template-columns: 1fr auto auto;
  gap: 16px;
  padding: 3px 0;
  color: var(--bb-ink-soft);
}
.dialog-body .upgrade-ledger li.total {
  margin-top: 4px;
  padding-top: 6px;
  border-top: 1px solid var(--bb-rule-soft);
  color: var(--bb-ink);
  font-weight: 600;
}
.dialog-body .upgrade-ledger .step { text-align: left; }
.dialog-body .upgrade-ledger .gp,
.dialog-body .upgrade-ledger .days { text-align: right; min-width: 5ch; }

/* Build milestones (#87) — since-strip name + Build-history ledger. The
 * since-strip reuses .defenses-line (square corners, flex-wrap stat row); the
 * ledger mirrors .upgrade-ledger's mono-number grammar. No new tokens. */
.milestone-line .milestone-name {
  font-family: var(--bb-font-italic);
  font-style: italic;
  color: var(--bb-ink);
}
.milestone-line .milestone-since-link {
  background: none;
  border: none;
  padding: 0;
  cursor: pointer;
  font-family: var(--bb-font-italic);
  font-style: italic;
  font-size: inherit;
  color: var(--bb-ink);
  text-decoration: underline dotted;
  text-underline-offset: 2px;
}
.milestone-line .milestone-since-link:hover { color: var(--bb-gilt-deep); }
.milestone-line .defenses-stats {
  font-variant-numeric: lining-nums tabular-nums;
}
.milestone-line .defenses-stats .none {
  font-family: var(--bb-font-italic);
  font-style: italic;
  color: var(--bb-ink-faint);
}

/* #89 — new-since-milestone floorplan highlight toggle */
/* The delta grows to fill the strip so the action pills right-align without
 * auto-margin juggling (mark-milestone affordance, item 2). */
.milestone-line .defenses-stats { flex: 1 1 auto; min-width: 0; }
.milestone-highlight-btn,
.milestone-mark-btn {
  margin-left: 8px;
  padding: 3px 8px;
  font-family: var(--bb-font-display);
  font-size: 0.6875rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  background: transparent;
  color: var(--bb-gilt-deep);
  border: 1px solid var(--bb-gilt);
  border-radius: 0;
  cursor: pointer;
  white-space: nowrap;
}
/* Reserve the wider state's width so "Show new" ⇄ "Showing new" never grows
 * the pill and bumps it to a new line (#218). */
.milestone-highlight-btn { min-width: 8.5em; text-align: center; }
.milestone-highlight-btn:hover,
.milestone-mark-btn:hover:not(:disabled) {
  background: rgba(168,120,38,0.08);
  color: var(--bb-gilt);
  border-color: var(--bb-gilt-bright);
}
/* Disabled mark pill (nothing new to capture) — same quiet treatment as
 * .add-btn:disabled, so the strip stays visible without inviting a dead click. */
.milestone-mark-btn:disabled {
  border-color: var(--bb-rule-soft);
  color: var(--bb-ink-faint);
  background: transparent;
  cursor: not-allowed;
}
.milestone-highlight-btn[aria-pressed="true"] {
  background: var(--bb-gilt-deep);
  color: var(--bb-parchment-light);
  border-color: var(--bb-gilt-deep);
}
.milestone-highlight-btn[aria-pressed="true"]:hover {
  background: var(--bb-gilt);
  border-color: var(--bb-gilt);
  color: var(--bb-ink);
}
/* Narrow tier: instead of one pill per line, the delta claims the full first
 * row and the two pills drop to a right-aligned pair on the second (#218).
 * Whichever pill leads the row gets the auto-margin that pushes the pair
 * right; when both are present the trailing mark pill keeps a plain gap.
 *
 * Deliberately WIDTH-ONLY — it does not carry the tier's height branch (#283).
 * This rule forces a wrap, i.e. it makes the row TALLER to fit a narrow column,
 * which is backwards on the axis a landscape phone has none of. At 828px the
 * pills fit on one line and should stay there. */
@media (max-width: 700px) {
  .milestone-line { row-gap: 6px; }
  .milestone-line .defenses-stats { flex-basis: 100%; }
  .milestone-line .milestone-highlight-btn,
  .milestone-line .milestone-mark-btn { margin-left: auto; }
  .milestone-line .milestone-highlight-btn ~ .milestone-mark-btn { margin-left: 8px; }
}

/* Gilt glow halo on newly-placed facility rects when highlight is active */
.fp-new-facility .facility-rect {
  filter: drop-shadow(0 0 3px var(--bb-gilt-bright))
          drop-shadow(0 0 7px var(--bb-gilt-bright));
}
/* Rail row → floorplan spotlight. Gilt fill + outlined stroke + double halo.
 * Same gilt vocabulary as .fp-new-facility but brighter+outlined so a transient
 * hover reads differently from the persistent new-placement glow. */
.facility-group.rail-lit .facility-rect {
  fill: var(--bb-gilt-fill);
  stroke: var(--bb-gilt-deep);
  stroke-width: 3;
  filter: drop-shadow(0 0 3px var(--bb-gilt-bright))
          drop-shadow(0 0 6px var(--bb-gilt-bright));
}
.fp-new-coord {
  filter: drop-shadow(0 0 3px var(--bb-gilt-bright))
          drop-shadow(0 0 7px var(--bb-gilt-bright));
}

/* Inline in the Chronicle chapter now (not a dialog): shed the .dialog-body
 * 60vh cap + nested scroll so the ledger flows with the page (same fix as the
 * Household roster, nit 9). */
#milestones-body { white-space: normal; max-height: none; overflow: visible; }

/* THE WORKS FRIEZE (#296) — see render-history.js for the ordering argument.
 * The band runs OLDEST → NEWEST and is RIGHT-ALIGNED in its track, so the Mark
 * action beneath its trailing edge never moves as phases accumulate. That is
 * the whole reason for `justify-content: flex-end` here; it is not centring.
 *
 * Plates butt together, borders collapsed, so the band reads as one continuous
 * object rather than a row of separate tiles — a frieze, not a card grid. */

/* ---- The Works frieze + phase records (#296) ---------------------------- */
/* THE TRACK IS FIXED, IN BOTH SHAPES, and that is what makes the right-aligned
 * band work. The band grows leftward from the track's trailing edge, so Mark
 * sits at one x forever — but only if the track itself has one width. On the
 * spread the facing page IS 372px, so the cap is invisible; on the leaf's 760px
 * measure, without it the band would be stranded 440px away from its own
 * heading with Mark against the page edge. Same objects, same size, refolded —
 * which is the invariant that makes the first Advance's reshape legal. */
.works-frieze { display: flex; flex-direction: column; align-items: stretch; gap: 6px; max-width: 372px; }
/* WRAPS, never scrolls. A right-aligned flex row that overflows does not
 * scroll into view — it overflows the START edge, so the oldest plates render
 * outside the track (measured: a 4-plate band put its first plate 67px left of
 * the facing page). Wrapping keeps every plate on the page and keeps the
 * trailing edge — where Mark lives — exactly where it was. */
.frieze-band {
  --frieze-plate: 108px;
  display: flex;
  flex-direction: row;
  flex-wrap: wrap;
  justify-content: flex-end;
  align-items: stretch;
  gap: 0;
}
/* DOUBLE-CLASSED ON PURPOSE, not for emphasis. `.milestone-plate` below carries
 * a literal `width: 108px` and sits LATER in the file, so a single-class
 * `.frieze-plate` loses the specificity tie and the custom property never
 * reaches the box. It looked correct at desk width purely because both numbers
 * were 108 — the phone tier's 88px override was silently dead, and the band
 * wrapped to two rows on a phone. Measured, not reasoned about. */
.milestone-plate.frieze-plate { flex: 0 0 var(--frieze-plate); width: var(--frieze-plate); height: var(--frieze-plate); }
.frieze-plate + .frieze-plate { margin-left: -1px; }
.frieze-plate.is-open { border-color: var(--bb-vermillion); z-index: 1; }
.frieze-plate.is-wip { border-style: dashed; border-color: var(--bb-gilt); background: var(--bb-gilt-fill); }
.frieze-plate.is-wip.is-open { border-color: var(--bb-vermillion); }
/* THE OVERFLOW MARKER — plates standing behind plates, drawn as stacked
 * hairlines rather than a fourth plate. A plate-shaped "more" would be a phase
 * that is not a phase; this is the fore-edge's own "N earlier…" rotated 90°.
 * Full plate height so the band's top and bottom edges stay unbroken. */
.frieze-more {
  flex: 0 0 30px;
  align-self: stretch;
  margin-right: -1px;
  padding: 0;
  background:
    repeating-linear-gradient(to right,
      var(--bb-rule-soft) 0 1px,
      transparent 1px 7px);
  border: none;
  border-radius: 0;
  box-shadow: none;
  cursor: pointer;
  opacity: 0.75;
}
.frieze-more:hover { opacity: 1; }
.frieze-more:focus-visible { outline: 2px solid var(--bb-gilt-deep); outline-offset: 1px; }
/* RIGHT-SET, and the one caption in the app that is. It cannot sit under its
 * own plate (108px wraps it to four lines), so it is a band-width line — and a
 * left-set line under a right-aligned band would read as belonging to whatever
 * stands at the LEFT end, which is the oldest phase rather than the newest.
 * The facing page's figures are already right-set, so it reads as native. */
.frieze-gloss {
  margin: 0;
  text-align: right;
  font-family: var(--bb-font-italic);
  font-style: italic;
  font-size: 0.8125rem;
  color: var(--bb-ink-soft);
}
.frieze-act { display: flex; justify-content: flex-end; }
/* A phase's RECORD, unrolled under the band — name, date, what changed, the
 * verbs. One at a time, the muster roll's grammar. No plate: it is standing in
 * the band directly above, marked open. */
.phase-record {
  display: flex;
  flex-direction: column;
  gap: 4px;
  margin-top: 10px;
  padding: 8px 10px;
  background: var(--bb-parchment-light);
  border: 1px solid var(--bb-rule-soft);
}
.phase-record.milestone-wip { border-style: dashed; border-color: var(--bb-gilt); background: var(--bb-gilt-fill); }
.phase-record .milestone-action { align-self: flex-start; font-size: 0.8125rem; }

@media (max-width: 700px), (max-height: 500px) and (pointer: coarse) {  /* tier (#283) */
  /* The band shrinks rather than dropping plates: three plates plus the marker
   * at 88px is 294px, inside a 390px screen's measure. Losing plates would mean
   * one render path per tier, and gaining a scroll would mean an action the
   * reader has to find by dragging. */
  .frieze-band { --frieze-plate: 88px; }
}
.milestone-plate {
  flex: 0 0 108px;
  width: 108px;
  height: 108px;
  align-self: flex-start;
  padding: 6px;
  display: grid;
  place-items: center;
  overflow: hidden;
  background: var(--bb-parchment-light);
  border: 1px solid var(--bb-rule-soft);
  border-radius: 0;
  box-shadow: none;
  cursor: pointer;
}
.milestone-plate:hover { border-color: var(--bb-gilt); background: var(--bb-gilt-fill); }
.milestone-plate:focus-visible { outline: 2px solid var(--bb-vermillion); outline-offset: 2px; }
.milestone-plate svg { display: block; max-width: 100%; max-height: 100%; }
.phase-record .ck-name {
  background: transparent;
  border: none;
  border-radius: 0;
  box-shadow: none;                 /* the global input's sunken well reads as a strip here */
  min-width: 0;
  padding: 2px 4px;
  margin: -2px -4px;
  color: var(--bb-ink);
  font-family: var(--bb-font-italic);
  font-style: italic;
  font-size: 0.9375rem;             /* 15px */
  width: 100%;
  box-sizing: border-box;
}
.phase-record .ck-name:hover {
  background: var(--bb-parchment-mid);
  cursor: text;
}
.phase-record .ck-name:focus {
  outline: none;
  background: var(--bb-parchment-mid);
}
/* Identity + delta on one line: the in-game date (italic, in-character) and
 * level left; the phase delta right as ONE quiet .turn-log-meta string
 * ("+17,500 gp · +700 d") — the same ledger grammar as the sealed turn rows.
 * "rebuilt" is the italic meta word for a phase that changed shape but cost
 * nothing. */
.milestone-id-row {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 12px;
  font-size: 0.8125rem;               /* 13px */
  color: var(--bb-ink-soft);
}
.milestone-identity { min-width: 0; }
.phase-record .is-rebuilt {
  font-family: var(--bb-font-italic);
  font-style: italic;
}
/* Footer: the real-world stamp, plain and faint (NOT italic — italic is the
 * in-character calendar's voice, plain is the system's), beside the one text
 * verb (delete, vermillion whisper). */
.milestone-footer { display: flex; justify-content: space-between; align-items: baseline; gap: 12px; margin-top: 2px; }
.milestone-marked { font-size: 0.75rem; color: var(--bb-ink-faint); font-variant-numeric: lining-nums tabular-nums; }
.milestone-action { font-size: 0.8125rem; }

/* The unsealed current phase (#198): the milestone-card anatomy wearing the
   ghost launcher's gilt-dashed grammar — dashed = not a record yet, solid =
   sealed. The eyebrow is real text so the unsealed state never rides on the
   border alone. */
.milestone-wip-eyebrow {
  font-family: var(--bb-font-display);
  font-size: 0.72rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--bb-gilt-deep);
}
/* The ghost launcher — the .reg-card-add tile at the top of the ledger (the
 * slot where the next milestone lands); deliberately lighter than the plate
 * cards: not a record yet. Doubles as the empty state. */
.milestone-launch { width: 100%; min-height: 64px; flex-direction: column; gap: 4px; }
.milestone-launch .milestone-launch-sub {
  font-family: var(--bb-font-italic);
  font-style: italic;
  font-size: 0.8125rem;
  letter-spacing: 0;
  color: var(--bb-ink-soft);
}
/* Resting states for The Works (#218) — the quiet stand-in for the retired
 * launcher when there's nothing to seal (no build yet, or nothing new since the
 * last milestone). A manuscript aside, not a card or a dead button: deliberately
 * faint so it never competes with the dashed-gilt in-progress card. */
.milestone-resting {
  font-family: var(--bb-font-italic);
  font-style: italic;
  font-size: 0.8125rem;
  color: var(--bb-ink-faint);
  padding: 6px 2px;
}
/* The in-progress card's footer is an action row now (it carries the Mark
 * milestone pill), not a passive caption — a little more air, and center the
 * pill against the VTT link. Scoped to the WIP card; sealed footers unchanged. */
.milestone-wip .milestone-footer { align-items: center; margin-top: 6px; }
@media (max-width: 700px), (max-height: 500px) and (pointer: coarse) {  /* tier — tap target (#283) */
  .milestone-wip .milestone-mark-btn { padding: 8px 14px; }  /* thumb-sized primary act */
}

/* ---- Milestone details + crest gallery ---------------------------------- */
.cpv-gallery { display: flex; align-items: center; gap: 8px; }
.cpv-floorplan { flex: 1; min-width: 0; max-height: 70vh; overflow: auto; text-align: center; }
.cpv-floorplan svg { max-width: 100%; height: auto; }
.cpv-nav-btn {
  flex-shrink: 0;
  background: transparent;
  border: none;
  box-shadow: none;
  font-size: 1.75rem;
  line-height: 1;
  color: var(--bb-ink-faint);
  cursor: pointer;
  padding: 0 2px;
}
.cpv-nav-btn:hover:not(:disabled) { color: var(--bb-ink); }
.cpv-nav-btn:disabled { opacity: 0.25; cursor: default; }
.cpv-actions { justify-content: space-between; }
.milestone-details {
  margin-top: 6px;
}
.milestone-details > summary {
  cursor: pointer;
  list-style: none;
  font-family: var(--bb-font-display);
  font-size: 0.8125rem;               /* 13px */
  letter-spacing: 0.08em;
  /* Neutral navigation ink — vermillion on this card is reserved for the
   * delete whisper (the chapter's destructive color-coding). */
  color: var(--bb-ink-soft);
}
.milestone-details > summary:hover { color: var(--bb-ink); }
.milestone-details > summary::-webkit-details-marker { display: none; }
.milestone-details > summary::before {
  content: "\25B8";                 /* ▸ */
  display: inline-block;
  margin-right: 6px;
  color: currentColor;
}
.milestone-details[open] > summary::before { content: "\25BE"; }  /* ▾ */
.milestone-details ul {
  list-style: none;
  margin: 6px 0 0;
  padding: 0 0 0 18px;
  font-size: 0.84rem;             /* 13.5px */
  color: var(--bb-ink-soft);
}
.milestone-details li { padding: 2px 0; }

.dialog-body .dialog-warn {
  display: block;
  margin-top: 4px;
  font-family: var(--bb-font-italic);
  font-style: italic;
  color: var(--bb-gilt-deep);
}

/* #221: advisory line in the change-type picker (a Garden regrows 21 days).
 * A non-interactive list row — the gilt italic voice, none of the affordance. */
#special-candidates .pick-type-note {
  font-family: var(--bb-font-italic);
  font-style: italic;
  font-size: 0.875rem;
  color: var(--bb-gilt-deep);
  cursor: default;
  pointer-events: none;
}


/* ---- The Household: hirelings + In Residence ---------------------------- */
/* Household portrait gallery — Chapter IV (#154). One card per hireling slot
 * on the .reg-card chassis: flat and always-editable, no accordion. The name
 * is the Charter bastion-name overlay pair (§5.4) at card scale — an
 * aria-hidden display div paints the vermillion versal at rest, a transparent
 * input sits inset:0 on top and inks in on focus — plus the empty-name state
 * the Charter never needs: an unnamed slot shows the input itself, visible,
 * with an italic placeholder, so the invitation has a real hit area. One
 * vermillion accent per card (the versal); the facility cartouche stays a
 * quiet ink link. The chapter region scrolls (.chapter overflow:auto) — the
 * list must NOT cap its own height.
 *
 * #218 took the EMPTY slots out of this grid. They were cards too, and the
 * grid held both a ~123px vacancy and a ~260px person, so most rows trailed a
 * card-and-a-half of dead parchment — 685px of a 1229px list on a 21-slot
 * bastion. `align-items: start` is what exposed it; stretching would only have
 * traded holes for over-tall empty boxes. The fix is not a better grid, it is
 * that a seat with nobody in it was never a portrait: untouched seats now live
 * in `.house-open` below and the gallery holds people, so every card in it is
 * the same shape and no hole can form. The dashed "provisional" vacancy
 * grammar went with them — a card here is now always somebody. */
.household-list,
.residence-list {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
  gap: 16px;
  align-items: start;
}
.house-card {
  gap: 6px;
  padding: 12px 14px 14px;
}
/* Register spotlight (#154) — the "Hirelings" fact-row jump lands here. */
.house-card.house-lit {
  border-color: var(--bb-gilt-deep);
  box-shadow: 0 0 0 2px var(--bb-gilt-bright), var(--shadow-card);
}

/* Facility cartouche: affiliation eyebrow + the jump back to the Register.
 * roll-die link idiom — the dotted gilt underline, not color, carries it. */
.house-cartouche {
  display: inline-flex;
  align-items: center;
  align-self: flex-start;
  gap: 6px;
  max-width: 100%;
  padding: 0;
  background: none;
  border: 0;
  cursor: pointer;
  color: var(--bb-ink-soft);
  font-family: var(--bb-font-display);
  font-variant: small-caps;
  font-size: 0.84rem;
  letter-spacing: 0.03em;
  text-align: left;
}
.house-cartouche-label {
  text-decoration: underline dotted var(--bb-gilt);
  text-underline-offset: 3px;
}
.house-cartouche:hover .house-cartouche-label,
.house-cartouche:focus-visible .house-cartouche-label { text-decoration-style: solid; }
.house-cartouche:hover,
.house-cartouche:focus-visible { color: var(--bb-gilt-deep); }
.house-cartouche:focus-visible { outline: 2px solid var(--bb-gilt-deep); outline-offset: 2px; }
.house-cartouche-glyph { flex: none; width: 20px; height: 20px; }
.house-cartouche-glyph .glyph-svg { display: block; width: 20px; height: 20px; }

/* The illuminated name. Wrap min-height keeps a hit area when empty. */
.house-name-wrap { position: relative; min-height: 42px; }
.house-name-display {
  min-height: 36px;
  padding: 2px 2px 0;
  font-family: var(--bb-font-display);
  font-size: 1.3rem;
  line-height: 1.25;
  color: var(--bb-ink);
  overflow-wrap: anywhere;
}
.house-name-display::first-letter {
  float: left;
  margin-right: 3px;
  font-family: var(--bb-font-display);
  font-size: 34px;
  line-height: 27px;
  color: var(--bb-vermillion);
  /* #218 — always a capital, like a scribe's illuminated initial. A lowercase
   * glyph sits at x-height inside the tall floated box, so it reads as dropping
   * to the second line; the uppercase form fills from cap-height near the top. */
  text-transform: uppercase;
}
.house-name {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  min-width: 0;
  padding: 2px 2px 0;
  font-family: var(--bb-font-display);
  font-size: 1.3rem;
  color: var(--bb-ink);
  caret-color: var(--bb-vermillion);
  background: transparent;
  border: 1px solid transparent;
  box-shadow: none;
}
.house-name::placeholder {
  font-family: var(--bb-font-italic);
  font-style: italic;
  font-size: 1rem;
  color: var(--bb-ink-faint);
}
/* Named: the overlay reads, the input goes glass until focused. */
.house-name-wrap.is-named .house-name { color: transparent; caret-color: transparent; }
.house-name-wrap.is-named .house-name:focus { color: var(--bb-ink); caret-color: var(--bb-vermillion); }
.house-name:focus {
  outline: none;
  background: var(--bb-parchment-light);
  border-color: var(--bb-vermillion);
  box-shadow: 0 0 0 2px rgba(168, 40, 29, 0.2);
}
.house-name-wrap:focus-within .house-name-display { visibility: hidden; }
/* The SRD glyph rides inside the display, but the absolute .house-name input
   overlays it — lift the glyph above the input so its click opens the stat
   card instead of falling through and starting a name edit. */
.house-name-display .ref-glyph { position: relative; z-index: 1; }

/* Name suggester (#208): the quill trigger on an unnamed name field. A real
   <button> (keyboard-reachable, unlike the aria-hidden ref-glyph), a direct
   child of the wrap — not nested in the display — so it survives :focus-within
   and stays pinned right; z-index lifts it above the inset:0 .house-name input
   so its click opens the popover instead of falling through into a name edit.
   Gilt (not the ref-glyph's vermillion) focus ring — the Household surface's
   controls all use a gilt ring. Hidden once the slot is named. */
.house-suggest {
  position: absolute;
  top: 50%;
  right: 4px;
  transform: translateY(-50%);
  z-index: 2;
  display: inline-flex;
  width: 1.05em;
  height: 1.05em;
  padding: 0;
  border: 0;
  background: none;
  color: var(--bb-ink-faint);
  line-height: 1;
  cursor: pointer;
}
.house-suggest .glyph-svg { display: block; width: 100%; height: 100%; }
.house-suggest:hover,
.house-suggest:focus-visible,
.house-suggest[aria-expanded="true"] { color: var(--bb-gilt-deep); }
/* The global button:active nudge REPLACES a transform, it doesn't add to it —
   without this override the press swaps translateY(-50%) for translateY(1px)
   and the button jumps half its height out from under the pointer, so the
   mouseup lands on the input and the click retargets past the delegated
   handler (the #227 cascade lesson, hit-test edition). */
.house-suggest:active {
  color: var(--bb-vermillion);
  transform: translateY(calc(-50% + 1px));
}
.house-suggest:focus-visible { outline: 2px solid var(--bb-gilt-deep); outline-offset: 2px; }
.house-name-wrap.is-named .house-suggest { display: none; }

/* Role / prose / status: seamless manuscript fields — the validated left-rule
 * + gilt-on-focus idiom, typeset until touched. */
.house-role,
.house-prose,
.house-status {
  width: 100%;
  padding: 0.3rem 0.5rem;
  background: var(--bb-parchment);
  border: 1px solid transparent;
  border-left: 2px solid var(--bb-rule-soft);
  border-radius: 0;
  color: var(--bb-ink);
  font-family: var(--bb-font-italic);
  font-size: 0.95rem;
  transition: background-color 90ms ease, border-color 90ms ease;
}
.house-role::placeholder,
.house-prose::placeholder,
.house-status::placeholder { color: var(--bb-ink-faint); font-style: italic; }
.house-role:hover,
.house-prose:hover,
.house-status:hover { border-left-color: var(--bb-gilt); }
.house-role:focus,
.house-prose:focus,
.house-status:focus {
  outline: none;
  border-color: var(--bb-rule);
  border-left-color: var(--bb-gilt-deep);
  background: var(--bb-parchment-light);
}
.house-prose { min-height: 72px; line-height: 1.45; resize: vertical; }
.house-status-row { display: flex; align-items: baseline; gap: 8px; margin-top: 2px; }
.house-status-kicker {
  flex: none;
  font-family: var(--bb-font-display);
  font-variant: small-caps;
  font-size: 0.8rem;
  letter-spacing: 0.04em;
  color: var(--bb-ink-faint);
}
.house-status { flex: 1 1 auto; font-size: 0.9rem; color: var(--bb-ink-soft); }

/* In Residence recruit cards: same chassis, legibly not hirelings — an
 * editable home-facility select in the cartouche slot, a kind chip instead
 * of a role line, and a remove × (catalog-derived hireling slots have none). */
.house-recruit-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
.house-home {
  min-width: 0;
  flex: 0 1 auto;
  padding: 0 0 1px;
  background: none;
  border: 0;
  border-bottom: 1px dotted var(--bb-gilt);
  color: var(--bb-ink-soft);
  font-family: var(--bb-font-display);
  font-variant: small-caps;
  font-size: 0.84rem;
  letter-spacing: 0.03em;
  cursor: pointer;
}
.house-home:hover { color: var(--bb-gilt-deep); border-bottom-style: solid; }
.house-home:focus-visible { outline: 2px solid var(--bb-gilt-deep); outline-offset: 2px; }
.house-remove {
  flex: none;
  padding: 0 0.2rem;
  background: none;
  border: 0;
  color: var(--bb-ink-faint);
  cursor: pointer;
  font-size: 1.1rem;
  line-height: 1;
}
.house-remove:hover { color: var(--bb-danger); }
/* THE STANDING LINE (#218) — a register line stating what this resident IS,
   under the versal stating what they are CALLED. It replaces the lone kind chip
   and pulls the home select out of the head, which leaves the head holding only
   verbs (send to VTT, strike) instead of mixing a data field in with them.

   The connectives are faint and the picked words are ink: the sentence reads as
   prose, and the three things you can change are the three things that look
   like ink. Measured on `.reg-card`'s ground (`--bb-parchment-light`, NOT the
   sunken `-mid`): ink-soft 6.33–8.00, ink-faint 5.13–5.91, both AA-body in all
   four themes. */
.house-standing {
  margin: 0.1rem 0 0.15rem;
  font-family: var(--bb-font-display);
  font-variant: small-caps;
  font-size: 0.84rem;
  letter-spacing: 0.03em;
  line-height: 1.7;
  color: var(--bb-ink-faint);
}
/* A picked word: the display span carries the look, an invisible native
   <select> is stretched over it. Native means the phone gets its real picker
   and AT gets a real control — unlike the versal's pointer-only glyph. An
   ordinary inline <select> was tried and fails: three chevrons turn the
   sentence into a form pretending to be prose. */
.house-pick { position: relative; display: inline-block; }
.house-pick-word {
  border-bottom: 1px dotted var(--bb-gilt);
  color: var(--bb-ink-soft);
}
.house-pick-sel {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  /* The global `input, select { min-width: 120px }` field floor beats any
     `width` and is not specificity-sensitive — without this the invisible
     control over a short word ("Ape") is 120px wide, overflows to the right,
     and swallows the clicks meant for the 📖 glyph beside it. */
  min-width: 0;
  max-width: 100%;
  margin: 0;
  padding: 0;
  border: 0;
  opacity: 0;
  /* The native popup is drawn by the browser from the SELECT's own computed
     background-color and color — `background: none` left it UA white on a
     parchment page. `opacity: 0` hides the closed control without reaching the
     popup, so these two are the popup's only styling seam. */
  background-color: var(--bb-parchment-light);
  color: var(--bb-ink);
  font-family: var(--bb-font-text);
  cursor: pointer;
  -webkit-appearance: none;
  appearance: none;
}
/* Hover/focus darkens the ink and thickens the rule — the state channel is
   weight and ink, never hue. `--bb-gilt-deep` as TEXT measures 4.30–4.35 in
   three of four themes on this ground (under AA-body), so the house pattern of
   going gilt on interaction would have made these words harder to read the
   moment you reached for them. */
.house-pick-sel:hover + .house-pick-word,
.house-pick-sel:focus-visible + .house-pick-word {
  border-bottom-style: solid;
  border-bottom-color: var(--bb-gilt-deep);
  color: var(--bb-ink);
}
.house-pick-sel:focus-visible + .house-pick-word { outline: 2px solid var(--bb-gilt-deep); outline-offset: 2px; }
.house-ref-slot { display: inline; }
.house-standing-add { white-space: nowrap; }
/* WHAT THEY ARE (#218) — a free-typed field, because the Menagerie RAW lets the
   DM allow creatures outside the book's list, so a closed picker would
   contradict the rules. It needs no overlay: the overlay idiom above exists
   only to hide a <select>'s chevron, and an <input> has none — so this sits
   inline in the sentence as itself, seamless until you reach for it.
   `min-width: 0` is mandatory: the global `input, select { min-width: 120px }`
   field floor beats `size` exactly as it beats `width`. */
.house-statted {
  min-width: 0;
  width: auto;
  padding: 0;
  background: none;
  border: 0;
  border-bottom: 1px dotted var(--bb-gilt);
  border-radius: 0;
  box-shadow: none;
  font-family: var(--bb-font-display);
  font-variant: small-caps;
  font-size: 0.84rem;
  letter-spacing: 0.03em;
  color: var(--bb-ink-soft);
}
.house-statted::placeholder {
  font-family: var(--bb-font-italic);
  font-style: italic;
  font-variant: normal;
  color: var(--bb-ink-faint);
}
.house-statted:hover { border-bottom-style: solid; border-bottom-color: var(--bb-gilt-deep); color: var(--bb-ink); }
.house-statted:focus {
  outline: 2px solid var(--bb-gilt-deep);
  outline-offset: 2px;
  border-bottom-style: solid;
  color: var(--bb-ink);
}

@media (pointer: coarse) {
  /* Each overlay select carries the 44px minimum from the global button/select
     rule, so the line needs room for those bands to sit side by side without
     one swallowing taps meant for the line above. */
  .house-standing { line-height: 2.3; }
}

.house-add { min-height: 96px; font-size: 0.95rem; }

/* ---- Places open (#218) ------------------------------------------------- */
/* The seats nobody has taken, out of the portrait gallery entirely.
 *
 * A row per facility — the cartouche's quiet ink voice on the left, its free
 * seats as numbered pips on the right. The pip keeps the dashed "provisional"
 * grammar the vacancy CARD used to carry, reduced to the one mark that still
 * earns its place, which is the whole argument of the split: a seat is a
 * place, not a person, so it gets a place's weight and not a portrait's. The
 * numeral is the slot ordinal, so a half-staffed Gaming Hall reads ① ③ ④.
 *
 * Deliberately hairline rows and NOT .reg-card: a second card family here
 * would restate at portrait weight exactly the thing this section exists to
 * stop restating. */
.house-open { margin-top: 26px; }
.house-open-head { display: flex; align-items: baseline; gap: 10px; }
.house-open-count {
  font-family: var(--bb-font-italic);
  font-style: italic;
  font-size: 0.8rem;
  letter-spacing: 0;
  text-transform: none;
  color: var(--bb-ink-faint);
}
.house-open-sub {
  margin: 0.15rem 0 12px;
  font-family: var(--bb-font-italic);
  font-style: italic;
  font-size: 0.875rem;
  color: var(--bb-ink-soft);
}
.house-open-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
  gap: 2px 24px;
}
.house-open-fac {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 7px 2px;
  border-bottom: 1px solid var(--bb-rule-soft);
}
.house-open-cart {
  display: inline-flex;
  align-items: center;
  /* Wraps (#218): the row's fixed half is its seats, so a facility with four
     of them leaves the cartouche ~120px — and an "Under construction" chip is
     flex:none and nowrap, which took all of it and clipped the NAME to nothing.
     The name is the row's identity and the chip is an annotation on it, so the
     chip drops to a second line rather than the name losing its width. The
     shrink allowance below is the last resort under that (a 320px phone with
     four 44px seats), where an ellipsised chip still beats a row that pushes
     the grid sideways. */
  flex-wrap: wrap;
  gap: 3px 7px;
  min-width: 0;
  font-family: var(--bb-font-display);
  font-variant: small-caps;
  font-size: 0.86rem;
  letter-spacing: 0.03em;
  color: var(--bb-ink-soft);
}
.house-open-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.house-open-cart .house-construction-chip {
  flex: 0 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
}
.house-open-glyph { flex: none; width: 17px; height: 17px; opacity: 0.7; }
.house-open-glyph .glyph-svg { display: block; width: 17px; height: 17px; }
.house-open-seats { display: inline-flex; gap: 5px; flex: none; }
.house-seat {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 26px;
  height: 26px;
  padding: 0;
  background: none;
  border: 1px dashed var(--bb-rule-soft);
  border-radius: 50%;
  box-shadow: none;
  font-family: var(--bb-font-display);
  font-size: 0.78rem;
  color: var(--bb-ink-faint);
  cursor: pointer;
}
.house-seat:hover,
.house-seat:focus-visible {
  border-style: solid;
  border-color: var(--bb-gilt-deep);
  background: var(--bb-parchment-light);
  color: var(--bb-gilt-deep);
}
.house-seat:focus-visible { outline: 2px solid var(--bb-gilt-deep); outline-offset: 2px; }
/* Read-only viewer: the same mark with none of the affordance — a <span>, so
 * it is not focusable and takes no coarse touch floor. It still counts the
 * empty seats, which is a fact about the snapshot worth reading. */
.house-seat.is-static { cursor: default; }
/* Register spotlight, strip half — the jump lands here for a facility nobody
 * has staffed, which has no card to light. A wash and a gilt rule, not the
 * card's ring: a ring around a full-bleed row reads as a box, not a highlight. */
.house-open-fac.house-lit {
  background: var(--bb-parchment-light);
  border-bottom-color: var(--bb-gilt-deep);
}

/* ---- Responsive: Places open -------------------------------------------- */
/* LAST in the section, after every base rule it overrides (a media query adds
 * no specificity, so this is source order doing the work).
 *
 * The pip is a <button>, so it already takes `min-height: 44px` from the
 * global coarse floor — but only the HEIGHT, which would leave a 26×44 pill.
 * The width has to be restated here to keep it round. Do NOT answer that by
 * putting `min-height: 0` on `.house-seat` above: `.house-seat` outranks the
 * global `button` selector, so it would defeat the touch floor everywhere. */
@media (pointer: coarse) {
  .house-seat { width: 44px; height: 44px; font-size: 0.9rem; }
  .house-open-fac { padding: 5px 2px; }
}
@media (max-width: 700px), (max-height: 500px) and (pointer: coarse) {
  .house-open-list { grid-template-columns: 1fr; }
}


/* ---- Chronicle › Stores + the facility roll (#296) ---------------------- */
/* Stores (#114, inline in Chronicle › Stores since the turn-page rework):
   hand-maintained status trackers + ledger. Each facility sits in its own
   sunken .cap-row card, headed by the roster's tinted glyph disc so the
   tracker↔facility link reads at a glance; the derived defender line reuses
   .cap-row too, so it reads as computed output, never an input. The host is
   a plain flow block — the chapter owns scrolling (the old 60vh well was the
   dialog's). Stores never lifts: one reading order in every layout (Turns,
   Works, Stores) so nothing teleports across the spread breakpoint. */
.stores-list { margin-top: 0.2rem; }
/* Rail liveness: when an edit on one surface write-throughs to its twin
   (sweep card ↔ Stores), the patched control flashes a brief gilt wash so
   the mirroring is visible instead of inferred. */
@keyframes bb-tracker-flash {
  from { background-color: color-mix(in srgb, var(--bb-gilt) 30%, transparent); }
  to { background-color: transparent; }
}
.tracker-flash { animation: bb-tracker-flash 1.1s ease-out; }
@media (prefers-reduced-motion: reduce) {
  .tracker-flash { animation: none; }
}
.stores-defenders { margin-bottom: 1rem; }
.stores-defenders.is-over .count-big { font-weight: 700; }
.stores-cap-max { color: var(--bb-ink-soft); }
.stores-over-note { color: var(--bb-gilt-deep); }
/* Defender meter: small ink-soft shield inline with the label — derived, so it
   gets a plain glyph, not the vermillion identity disc the facility cards use. */
.stores-meter-label { display: inline-flex; align-items: center; gap: 0.4rem; }
.stores-meter-icon { display: inline-grid; place-items: center; width: 16px; height: 16px; color: var(--bb-ink-soft); }
.stores-meter-icon .glyph-svg { width: 16px; height: 16px; display: block; fill: currentColor; }

/* Facility status card — .cap-row provides the sunken parchment chassis. Still
 * worn by the turn page's sweep cards; the Stores facilities gave it up for the
 * roll in #296 (see .stroll-fac below). */
.stores-card { margin-bottom: 0.75rem; }
.stores-card-head { display: flex; align-items: center; gap: 0.55rem; margin-bottom: 0.5rem; padding-bottom: 0.4rem; border-bottom: 1px solid var(--bb-rule-soft); }
.stores-card-name { font-family: var(--bb-font-display); font-size: 1.0625rem; color: var(--bb-ink); }

/* THE FACING PAGE'S ROW (#296) — label left, figure right, ruled. ONE grammar
   shared by Stores and the Reckoning, so the right-hand page reads as one page
   in two states rather than as two designs that happen to be adjacent. This is
   the same label|figure shape as the muster roll opposite it, at the facing
   page's narrower measure. */
.stroll-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: baseline;
  column-gap: 0.8rem;
  padding: 0.3rem 0;
  border-bottom: 1px dotted var(--bb-rule-soft);
}
.stroll-label { font-family: var(--bb-font-display); font-size: 0.9rem; color: var(--bb-ink-soft); min-width: 0; }
.stroll-value { font-variant-numeric: lining-nums tabular-nums; color: var(--bb-ink); text-align: right; }

/* DECLARED HERE, ABOVE BOTH ITS USERS, AND THAT PLACEMENT IS LOAD-BEARING. Every
   specialization below (the Stores head's reversed track, the phone tier's
   stack, the Reckoning's ruled-off net) is a single class competing with this
   one, so a base defined AFTER them silently wins every tie — which it did:
   "War Room #1" rendered as "W #1" and the phone tier's two-line stack never
   fired at all, both from source order alone. Keep the shared grammar first. */

/* STORES IN THE ROLL GRAMMAR (#296) — one line per facility, its name and what
 * it holds, with the four editors unrolling beneath. Ten cards in a 340px rail
 * measured 1697px against a turn page phase 1 had already brought to ~540px:
 * the rail was twice the page, inverted. The `.stroll-row` shape itself is
 * shared with the Reckoning (see its definition) so the facing page reads as
 * one page in two states.
 *
 * The head is a BUTTON, and the whole row is the target — the summary is as
 * much the affordance as the name, because "12 / 12 defenders" is what the
 * reader is reaching for when they open the row. */
.stroll-fac { border-bottom: 1px dotted var(--bb-rule-soft); }
.stroll-fac:last-of-type { border-bottom: none; }
button.stroll-head {
  width: 100%;
  background: none;
  border: none;
  border-radius: 0;
  box-shadow: none;
  cursor: pointer;
  color: inherit;
  text-align: left;
  font: inherit;
  padding: 0.32rem 0.2rem;
  border-bottom: none;
}
button.stroll-head:hover { background: var(--bb-parchment-mid); }
button.stroll-head:focus-visible { outline: 2px solid var(--bb-gilt-deep); outline-offset: -2px; }
/* THE NAME SIZES ITSELF, THE SUMMARY TAKES THE REST. `.stroll-row`'s own
 * `1fr auto` is right for a figure (a label with room, a number that never
 * wraps) and wrong for a sentence: a grid `auto` track sizes to max-content
 * while `1fr` shrinks below its own, so "3 / 10 lieutenants · Mounted · 14
 * mustered guards" took the row and left "War Room #1" rendering as "W #1".
 * Reversed, the name keeps its width and the summary wraps — which is the
 * right thing to give way, since it is prose. */
.stroll-head { grid-template-columns: auto minmax(0, 1fr); }
.stroll-head .stroll-label { display: flex; align-items: center; gap: 0.45rem; min-width: 0; font-size: 1rem; }
.stroll-head .stores-card-name { font-size: 1rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.stroll-summary { font-family: var(--bb-font-display); font-size: 0.85rem; color: var(--bb-ink-soft); text-align: right; }
.stroll-summary.is-empty { color: var(--bb-ink-faint); }
/* The open row keeps its summary — it is the sentence the reader is editing,
 * the same argument as the loadout shelf's own summary line staying visible
 * above the open list. Quieted, because the controls below now say it in full. */
.stroll-fac.is-open > .stroll-head { background: var(--bb-parchment-mid); }
.stroll-fac.is-open .stroll-summary { color: var(--bb-ink-faint); }
.stroll-body { padding: 0.35rem 0.2rem 0.6rem; }
.stroll-body .stores-row:last-child { margin-bottom: 0; }
/* A viewer's rows are always open and there is no disclosure, so the head is a
 * plain heading rather than a control (see facilityCard). */
.stroll-fac > .stroll-head:not(button) { padding: 0.32rem 0.2rem; border-bottom: 1px solid var(--bb-rule-soft); }
.stores-card-ord { font-family: var(--bb-font-display); font-size: 0.875rem; color: var(--bb-ink-soft); font-variant-numeric: lining-nums tabular-nums; }

.stores-row { display: grid; grid-template-columns: 1fr auto; gap: 0.35rem 0.5rem; align-items: baseline; margin-bottom: 0.3rem; }
.stores-row--stacked { grid-template-columns: 1fr; gap: 0.2rem; }
.stores-label { font-family: var(--bb-font-display); font-variant: small-caps; font-size: 0.8rem; color: var(--bb-ink-faint); letter-spacing: 0.04em; }
/* Dotted ledger leader between label and control — the character-sheet texture. */
.stores-row:not(.stores-row--stacked) .stores-label { display: flex; align-items: baseline; white-space: nowrap; }
.stores-row:not(.stores-row--stacked) .stores-label::after { content: ""; flex: 1 1 auto; margin-left: 0.5em; border-bottom: 1px dotted var(--bb-rule-soft); transform: translateY(-0.18em); }
.stores-counter-field { display: inline-flex; align-items: baseline; gap: 0.3rem; justify-self: end; }
/* Frame + focus from Archetype B (the ruled slot). Ink numerals at a calmer
   1.05rem — the card's figure, no longer a vermillion shout in a white box. */
.stores-num { width: 3.25rem; text-align: right; font-family: var(--bb-font-display); font-size: 1.05rem; font-variant-numeric: lining-nums tabular-nums; padding: 0.1rem 0.35rem; }
.stores-max { font-family: var(--bb-font-display); font-size: 0.9375rem; color: var(--bb-ink-soft); font-variant-numeric: lining-nums tabular-nums; }
.stores-toggle { display: inline-flex; gap: 0; justify-self: end; }
.stores-toggle-btn { font-size: 0.84rem; padding: 2px 10px; }
.stores-toggle-btn + .stores-toggle-btn { margin-left: -1px; }
.stores-note { font-family: var(--bb-font-italic); font-size: 0.9375rem; padding: 0.2rem 0.4rem; width: 100%; }
.stores-row:not(.stores-row--stacked) .stores-note { min-width: 12rem; justify-self: end; }
textarea.stores-note { resize: vertical; }

/* Read-only viewer readouts (#254): the static twin of every tracker control
   in a shared snapshot. Deliberately NOT a greyed-out field — a frame with no
   ink in it reads as "you may type here, but not yet", and there is no "yet".
   The value inherits the control's own type (display face + tabular figures
   for numbers, the italic hand for text) and simply sits where the control
   sat, so a card keeps its shape between the player's screen and the DM's. */
.stores-readout { font-family: var(--bb-font-display); font-size: 1.05rem; font-variant-numeric: lining-nums tabular-nums; color: var(--bb-ink); }
.stores-readout--text { font-family: var(--bb-font-italic); font-size: 0.9375rem; font-variant-numeric: normal; }
/* Direct children only — the counter's readout is nested inside
   .stores-counter-field, which already carries the end-alignment. Each readout
   lands exactly where the control it replaced sat, so a card doesn't reflow
   between the player's screen and the DM's. */
.stores-row:not(.stores-row--stacked) > .stores-readout { justify-self: end; text-align: right; }
.stores-readout.is-empty { color: var(--bb-ink-faint); }

/* Loadout picks (#221, rebuilt as a stock list in #218). The closed row is what
 * the facility HOLDS, in the same words the Register card, the handout and a
 * shared snapshot print — so it borrows `.stores-readout--text`'s italic hand
 * rather than inventing a face. The catalog unrolls beneath it as a ruled
 * shelf, where an option's effect is body text instead of a hover tooltip: the
 * old chips hid it from every phone and every screen reader. */
.stores-loadout { display: flex; flex-direction: column; gap: 0.3rem; align-items: stretch; }
.stores-shelf-summary { display: flex; align-items: baseline; gap: 0.5rem; width: 100%; background: none; border: none; box-shadow: none; padding: 0.1rem 0; text-align: left; cursor: pointer; color: inherit; }
.stores-shelf-value { flex: 1 1 auto; font-family: var(--bb-font-italic); font-size: 0.9375rem; color: var(--bb-ink); }
.stores-shelf-value.is-empty { color: var(--bb-ink-faint); }
.stores-shelf-caret { flex: 0 0 auto; color: var(--bb-ink-faint); font-size: 0.8rem; transition: transform 0.12s ease; }
.stores-loadout.is-open .stores-shelf-caret { transform: rotate(90deg); }
.stores-shelf-summary:hover .stores-shelf-caret { color: var(--bb-gilt-deep); }
.stores-shelf-summary:focus-visible { outline: 2px solid var(--bb-gilt-deep); outline-offset: 1px; }

/* The shelf. `hidden` alone loses to the display rule, so it is spelled out. */
.stores-shelf { display: flex; flex-direction: column; border-top: 1px solid var(--bb-rule-soft); padding-top: 0.25rem; }
.stores-shelf[hidden] { display: none; }
.stores-shelf-row { display: grid; grid-template-columns: 1rem 1fr; gap: 0 0.5rem; align-items: baseline; width: 100%; background: none; border: none; box-shadow: none; border-bottom: 1px dotted var(--bb-rule-soft); padding: 0.28rem 0.1rem; text-align: left; color: inherit; }
.stores-shelf-row:last-of-type { border-bottom: none; }
button.stores-shelf-row { cursor: pointer; }
button.stores-shelf-row:hover { background: var(--bb-parchment-light); }
button.stores-shelf-row:focus-visible { outline: 2px solid var(--bb-gilt-deep); outline-offset: -2px; }
.stores-shelf-box { width: 0.8rem; height: 0.8rem; border: 1px solid var(--bb-ink-soft); border-radius: 2px; align-self: center; }
.stores-shelf-tick.active .stores-shelf-box { background: var(--bb-gilt-deep); border-color: var(--bb-gilt-deep); box-shadow: inset 0 0 0 2px var(--bb-parchment-light); }
.stores-shelf-label { font-size: 0.9375rem; color: var(--bb-ink); }
.stores-shelf-tick.active .stores-shelf-label { font-weight: 600; }
.stores-shelf-hint { grid-column: 2; font-family: var(--bb-font-italic); font-style: italic; font-size: 0.82rem; color: var(--bb-ink-soft); }

/* A write-in is the DM's own option. It has no tick box because it cannot be
 * unticked — it exists or it doesn't — so `×` is its only verb, borrowed whole
 * from the ledger row on the same card. */
/* Three columns, not two: the label has to land in the same place as a catalog
   row's or the shelf reads ragged where the DM's own options begin. */
.stores-shelf-row.is-written { grid-template-columns: 1rem 1fr 1.2rem; }
.stores-shelf-row.is-written .stores-shelf-label { grid-column: 2; font-weight: 600; }
.stores-shelf-del { background: none; border: none; box-shadow: none; color: var(--bb-ink-faint); cursor: pointer; font-size: 1.05rem; line-height: 1; padding: 0 0.2rem; }
.stores-shelf-del:hover { color: var(--bb-danger); }
.stores-shelf-del:focus-visible { outline: 2px solid var(--bb-gilt-deep); outline-offset: 1px; }
.stores-shelf-write { border-top: 1px solid var(--bb-rule-soft); border-bottom: none; margin-top: 0.15rem; }
.stores-shelf-quill { color: var(--bb-ink-faint); font-size: 0.85rem; }
.stores-shelf-write .stores-shelf-label { font-family: var(--bb-font-italic); font-style: italic; color: var(--bb-ink-soft); }
.stores-shelf-write:hover .stores-shelf-label { color: var(--bb-ink); }
.stores-shelf-input { width: 100%; font-family: var(--bb-font-italic); font-size: 0.9375rem; padding: 0.2rem 0.4rem; margin-top: 0.15rem; }

.stores-loadout-status { font-family: var(--bb-font-italic); font-style: italic; font-size: 0.85rem; color: var(--bb-ink-soft); padding-bottom: 0.2rem; }
.stores-loadout-status.is-open { color: var(--bb-gilt-deep); }
.stores-loadout-note { font-family: var(--bb-font-italic); font-style: italic; font-size: 0.82rem; color: var(--bb-ink-soft); padding-top: 0.25rem; }
.reg-loadout-due { opacity: 0.75; }

/* Empty state — a quiet centred crate so a fresh bastion still looks designed. */
.stores-empty { display: flex; flex-direction: column; align-items: center; text-align: center; gap: 0.5rem; padding: 1.25rem 0.5rem; }
.stores-empty-icon { display: inline-grid; place-items: center; width: 40px; height: 40px; color: var(--bb-ink-faint); opacity: 0.7; }
.stores-empty-icon .glyph-svg { width: 40px; height: 40px; display: block; fill: currentColor; }
.stores-empty .empty-line { margin: 0; max-width: 30rem; }
/* The ledger is free-form and never facility-gated — set its card apart from
   the facility cards with extra space above. */
.stores-ledger { margin-top: 1.25rem; }
.stores-ledger-row { display: grid; grid-template-columns: 1fr 1fr 1.5rem; gap: 0.4rem; align-items: center; margin-bottom: 0.3rem; }
.stores-ledger-label, .stores-ledger-value { font-family: var(--bb-font-italic); font-size: 0.9375rem; padding: 0.2rem 0.4rem; width: 100%; }
.stores-ledger-del { background: none; border: none; color: var(--bb-ink-faint); cursor: pointer; font-size: 1.1rem; line-height: 1; padding: 0 0.2rem; }
.stores-ledger-del:hover { color: var(--bb-danger); }
.stores-ledger-del:focus-visible { outline: 2px solid var(--bb-gilt-deep); outline-offset: 1px; }
.stores-add-row { background: none; border: none; padding: 0; margin-top: 0.4rem; cursor: pointer; font-family: var(--bb-font-italic); font-style: italic; font-size: 0.9375rem; color: var(--bb-ink-soft); text-decoration: underline dotted; text-underline-offset: 2px; box-shadow: none; }
.stores-add-row:hover { color: var(--bb-ink); }

/* Derived turn-tally block (#191) */
.stores-derived-marker { font-variant: small-caps; font-size: 0.8rem; color: var(--bb-ink-faint); margin-bottom: 0.35rem; }
.stores-derived-block { margin-bottom: 0.25rem; }
.stores-derived-row { display: grid; grid-template-columns: 1fr auto; align-items: baseline; padding: 0.15rem 0; }
.stores-derived-label { font-family: var(--bb-font-italic); font-style: italic; font-size: 0.9375rem; color: var(--bb-ink); min-width: 0; }
.stores-derived-value { font-family: var(--bb-font-display); font-size: 1.1rem; font-variant-numeric: lining-nums tabular-nums; text-align: right; color: var(--bb-ink); white-space: nowrap; }
/* Goods ON HAND, not goods ever produced: the count is a field, because a thing
   the bastion made leaves it (onto a character sheet, and then spent or brought
   home) and none of that is a Bastion Turn action. Sized to the treasury field
   it rhymes with — it is the same anchoring gesture, for items. */
.stores-derived-field { display: flex; justify-content: flex-end; }
.stores-stock-input { width: 5rem; text-align: right; }
/* An anchored row wears the gilt marginal the standing strip uses for "somebody
   has touched this" — it distinguishes a figure the player set from one the
   turns produced, without a second column or a word. */
.stores-derived-row.is-anchored { box-shadow: inset 2px 0 0 var(--bb-gilt); padding-left: 0.4rem; }
.stores-ledger-split { border: none; border-top: 1px solid var(--bb-rule-soft); margin: 0.5rem 0 0.35rem; }

/* Treasury (#191) */
.stores-treasury { display: grid; grid-template-columns: 1fr auto; align-items: baseline; margin-bottom: 0.15rem; }
.stores-treasury-label { font-variant: small-caps; font-size: 0.9rem; color: var(--bb-ink-faint); }
.stores-treasury-field { display: inline-flex; align-items: baseline; gap: 0.35rem; }
.stores-treasury-field .stores-num { width: 4.5rem; }
.stores-treasury-unit { font-size: 0.875rem; color: var(--bb-ink-soft); }
.stores-treasury-sub { font-family: var(--bb-font-italic); font-style: italic; font-size: 0.8125rem; color: var(--bb-ink-faint); display: flex; flex-wrap: wrap; gap: 0.3rem; align-items: baseline; margin-bottom: 0.5rem; }
.stores-treasury-stop { background: none; border: none; padding: 0; cursor: pointer; font-family: var(--bb-font-italic); font-style: italic; font-size: 0.8125rem; color: var(--bb-ink-soft); text-decoration: underline dotted; text-underline-offset: 2px; box-shadow: none; }
.stores-treasury-stop:hover { color: var(--bb-ink); }

/* ---- Responsive: Stores rows stack -------------------------------------- */
/* Tracker labels are user-authored, so the label column cannot be trusted
 * to stay short. Stacking is a Stores decision and now lives with Stores. */
@media (max-width: 700px), (max-height: 500px) and (pointer: coarse) {
  /* --- Stores rows stack (#158): tracker labels are user-authored and the
   * non-stacked regime pins them nowrap beside a min-width control — a long
   * label is guaranteed horizontal overflow at 375px. Mirror the existing
   * .stores-row--stacked look (label above control) and neutralize the
   * :not(--stacked) leader/nowrap/min-width rules that would otherwise
   * still bite. --- */
  /* The facility roll stacks too. At 346px the name column shrinks to
   * min-content and "War Room #1" renders as "#1" — the summary is prose and
   * wins the squeeze, which is right on a wide page and useless on a narrow
   * one. Two lines, name first: the row is still one object, and both halves
   * are whole. This has to live HERE, below `.stroll-row`, and not up with the
   * frieze's own tier block — same single-class specificity, so above it the
   * override never fired. */
  .stroll-head { grid-template-columns: minmax(0, 1fr); row-gap: 0.1rem; }
  .stroll-head .stroll-summary { text-align: left; }

  .stores-row { grid-template-columns: 1fr; gap: 0.2rem; }
  .stores-row:not(.stores-row--stacked) .stores-label {
    display: block;
    white-space: normal;
  }
  .stores-row:not(.stores-row--stacked) .stores-label::after { content: none; }
  .stores-row:not(.stores-row--stacked) .stores-note {
    min-width: 0;
    justify-self: stretch;
  }
  /* Same treatment for the viewer's static twin of a free-text tracker (#254):
     a note that wrapped to two lines would otherwise sit ragged-right under a
     left-aligned label. Numbers and toggle states stay end-aligned, matching
     the live controls beside them. */
  .stores-row:not(.stores-row--stacked) > .stores-readout--text {
    justify-self: stretch;
    text-align: left;
  }
}

/* ---- Bastion Turn page (#112) ------------------------------------------- */
/* Top-bar cartouche: status + jump to this turn's Chronicle page. .btn-ghost
   supplies the chrome; the number rides the ledgers' tabular-numeral family
   (Arabic — Roman ordinals stay reserved for the five fixed chapters). The
   pip + "· N ready" suffix are the .has-vacancy gilt register — "something
   finished, gentle good news", never danger. */
.turn-cartouche { position: relative; text-transform: none; letter-spacing: 0.02em; font-variant-numeric: lining-nums tabular-nums; }
/* A typed week name is the player's own words and can run to sixty characters
   (#311); the cartouche clamps it rather than letting it push the top bar
   sideways. A derived date is bounded by the calendar's month names and never
   clips at this width. */
.turn-cartouche-week.is-typed { display: inline-block; max-width: 16ch; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; vertical-align: bottom; }
.turn-cartouche .turn-caret { color: var(--bb-ink-soft); margin-left: 0.15rem; }
.turn-cartouche.has-completions::after { content: ""; position: absolute; top: 2px; right: 2px; width: 8px; height: 8px; border-radius: 50%; background: var(--bb-gilt); }
.turn-cartouche-ready { color: var(--bb-gilt-deep); }

/* The open page (#turn-page): the live turn at the top of Chronicle › The
   Bastion Turns — unframed (the roll IS the page body; a wrapping border would
   be border-in-border). Reading order: head → standing strip → the muster roll
   → event form → tally + Advance, then the "Earlier turns" divider and the
   sealed log. Once live, the whole section rises to the top of the Chronicle
   column.

   --roll-cols is THE COLUMN TRACK OF THE WEEK'S PAGE, declared here once and
   consumed by both `.turn-fac-row` and `.turn-standing-row`. See the roll
   block below for why that is one declaration and not two. The glyph column is
   sized so the strip's 4px marginal stroke clears the centred glyph. */
#chronicle-turns { --roll-act: 10.5rem;
  --roll-cols: 1.5rem minmax(6rem, 12rem) minmax(0, 1fr) var(--roll-act); }
.turn-page:not(:empty) { margin: 0.2rem 0 0.4rem; }
.turn-page-head { margin-bottom: 0.9rem; }
.turn-page-title-row { display: flex; flex-wrap: wrap; align-items: baseline; justify-content: space-between; gap: 0.4rem 1rem; }
.turn-page-title { display: flex; align-items: center; gap: 0.1rem 0.55rem; font-family: var(--bb-font-display); font-size: 1.25rem; font-weight: normal; color: var(--bb-ink); margin: 0; }
/* THE ILLUMINATED VERSAL (#296) — Chronicle's missing display type. Every other
   chapter opens on something big: a crest and 32px vermillion numerals, the
   floorplan, an icon-led card grid. This chapter opened on a 13px small-caps
   label and set its own turn number smaller than the top-bar cartouche that
   points at it. Vermillion, matching the Charter's numerals, because the live
   week is the one thing on the page that is happening now. */
.turn-versal {
  font-family: var(--bb-font-display);
  font-size: 2.6rem;
  line-height: 0.85;
  color: var(--bb-vermillion);
  font-variant-numeric: lining-nums;
}
.turn-versal-stack { display: flex; flex-direction: column; gap: 0.05rem; }
.turn-versal-word { font-variant: small-caps; letter-spacing: 0.06em; font-size: 0.95rem; color: var(--bb-ink-soft); line-height: 1.1; }
.turn-versal-date { font-size: 0.875rem; line-height: 1.1; }
.turn-versal-date .turn-date-btn { font-size: 0.875rem; }
.turn-page.is-collapsed .turn-versal { font-size: 1.5rem; }
/* Fold caret: the open page can collapse to its head line for reference
   browsing (echoing the sealed rows' disclosure grammar). Class flip only —
   .is-collapsed hides everything but the head, and quiets the head's
   interactive whispers so the folded line reads as a summary, not a form. */
.turn-page-toggle { background: none; border: none; box-shadow: none; padding: 0 0.3rem 0 0; cursor: pointer; font-size: 0.9rem; color: var(--bb-ink-soft); align-self: center; }
.turn-page-toggle:hover { color: var(--bb-ink); }
.turn-page-toggle:focus-visible { outline: 2px solid var(--bb-gilt-deep); outline-offset: 1px; }
.turn-page.is-collapsed > :not(.turn-page-head) { display: none; }
.turn-page.is-collapsed .turn-page-head { margin-bottom: 0.2rem; }
.turn-page.is-collapsed .turn-progress,
.turn-page.is-collapsed .turn-rewind,
.turn-page.is-collapsed .turn-maintain-link,
.turn-page.is-collapsed .turn-maintain-prompt,
.turn-page.is-collapsed .turn-date-btn:not(.has-date) { display: none; }
.turn-page.is-collapsed .turn-date-btn { pointer-events: none; text-decoration: none; }
/* Folded one-liner: the sweep's state echoes into the title row (a twin of
 * the sealed log rows' meta, so the fold rhymes with the ledger below it). */
.turn-fold-echo { display: none; font-family: var(--bb-font-display); font-size: 0.875rem; color: var(--bb-ink-soft); font-variant-numeric: lining-nums tabular-nums; white-space: nowrap; }
.turn-page.is-collapsed .turn-fold-echo { display: inline; }
/* Inline in-game date on the title line: quiet whisper button ↔ transparent
   input (the milestone date-slot pattern) — commit on blur/Enter, Esc reverts. */
.turn-date-btn { background: none; border: none; box-shadow: none; padding: 0; cursor: pointer; font-family: var(--bb-font-italic); font-style: italic; font-size: 0.9375rem; color: var(--bb-ink-faint); text-decoration: underline dotted; text-underline-offset: 2px; }
.turn-date-btn:hover { color: var(--bb-ink-soft); }
.turn-date-btn.has-date { color: var(--bb-ink-soft); text-decoration: none; }
.turn-date-btn.has-date:hover { color: var(--bb-ink); text-decoration: underline dotted; text-underline-offset: 2px; }
.turn-date-btn:focus-visible { outline: 2px solid var(--bb-gilt-deep); outline-offset: 2px; }
.turn-date-input { width: 16rem; max-width: 60vw; background: transparent; border: none; border-bottom: 1px dotted var(--bb-rule); border-radius: 0; color: var(--bb-ink-soft); font-family: var(--bb-font-italic); font-style: italic; font-size: 0.9375rem; padding: 0.1rem 0; }
/* THE WEEK'S LOG (#312) — the account of the week, on every week, sealed or
   live. Two read states and one field, in the dateline's own grammar:
     ask   — no log. One dotted question in the versal stack, the "add a date"
             slot and voice. Drawing the frame empty instead cost +47px on every
             live head and made a frame mean nothing; this way a drawn frame
             always means a note exists.
     lead  — a log. The title row's next sibling, in the Household's written-
             line frame (Archetype A, further down this section, which is where
             the ground, the left rule and its gilt focus come from) at body
             size in the italic hand. Ruled paper and a display-size lead were
             both rendered and declined: the second because prose set as a
             heading invites a heading ("Session 4").
     field — the lead's exact padding, face and line-height, so the caret lands
             where the words were read from; `min-height` as the Charter's lore
             field, so three words are edited in a two-line box.
   `max-width: 62ch` on all three: a log is a memo at the head of the sheet,
   not a banner across it. No cap on the text and no fold — 685 characters on
   a 390px phone is twelve lines and reads as a diary page, which it is. */
.week-name-btn { background: none; border: none; box-shadow: none; padding: 0; margin: 3px 0 0; display: block; text-align: left; cursor: pointer; font-family: var(--bb-font-italic); font-style: italic; font-size: 0.9rem; line-height: 1.45; color: var(--bb-ink-faint); text-decoration: underline dotted; text-underline-offset: 3px; max-width: 62ch; }
.week-name-btn:hover { color: var(--bb-ink-soft); }
.week-name-btn:focus-visible { outline: 2px solid var(--bb-gilt-deep); outline-offset: 2px; }
.week-lead,
.week-field { display: block; width: 100%; max-width: 62ch; min-width: 0; box-sizing: border-box; margin: 8px 0 4px; padding: 7px 12px; font-family: var(--bb-font-italic); font-style: italic; font-size: 0.95rem; line-height: 1.5; }
.week-lead { cursor: text; white-space: pre-wrap; overflow-wrap: anywhere; }
/* Soft ink at rest, full ink under the pointer — the dateline's own pairing.
   Scoped under the head at (0,2,0) because the Archetype A group 1,300 lines
   on also sets `color` on `.week-lead` at (0,1,0), and there it wins by source
   order; a bare `.week-lead { color }` here was rendered at full ink with a
   hover that did nothing (found in review). */
.turn-page-head .week-lead { color: var(--bb-ink-soft); }
.turn-page-head .week-lead:hover { color: var(--bb-ink); }
.week-lead:focus-visible { outline: 2px solid var(--bb-gilt-deep); outline-offset: 2px; }
.week-field { min-height: 3.2em; resize: none; overflow: hidden; border-radius: 0; box-shadow: none; }
/* Archetype A's focus is a warmed border on a lightened ground, no halo — but
   the global `textarea:focus` halo is (0,1,1) and beats the bare class, so it
   is cleared here at (0,2,0) (the `.writ-slot:focus` precedent). */
.week-field:focus { box-shadow: none; }
.week-field::placeholder { color: var(--bb-ink-faint); font-style: italic; }
/* Folded, the head is a summary line: the log and its ask fold with the rest
   (measured — a folded live week otherwise carried a 51px paragraph). */
.turn-page.is-collapsed .week-name-btn,
.turn-page.is-collapsed .week-lead,
.turn-page.is-collapsed .week-field { display: none; }
.turn-date-input:focus { outline: none; border-bottom-color: var(--bb-ink-soft); color: var(--bb-ink); }
.turn-date-input::placeholder { color: var(--bb-ink-faint); }
/* THE STRUCTURED DATELINE (#310) — the read line swapped for its own controls,
   in place. Not a popover: that shape rendered as a ~200px form dropped across
   the standing charges to answer "when did this week begin?".
   Everything here sits on one line at 1440px and wraps to two at 390px. */
.turn-date-edit { display: inline-flex; align-items: baseline; flex-wrap: wrap; gap: 0.3rem; }
.turn-date-day,
.turn-date-year {
  /* The global 120px input floor beats any `width`, so these must clear it the
     way `.turn-duration` already does — `flex: none` plus an explicit width.
     2.8rem, not 2.6rem: the day field measured 42px under coarse emulation and
     the floor is 44. */
  flex: none;
  width: 2.8rem;
  min-width: 0;
  background: transparent;
  border: none;
  border-bottom: 1px dotted var(--bb-rule);
  border-radius: 0;
  padding: 0.1rem 0;
  text-align: right;
  font-family: var(--bb-font-italic);
  font-style: italic;
  font-size: 0.9375rem;
  color: var(--bb-ink-soft);
}
.turn-date-year { width: 3.6rem; }
.turn-date-month {
  /* Pinned, or the select auto-sizes to its longest option — "The Feast of the
     Moon" measures 155px and shoves the echo off the line. A native <select>
     also gives a thumb wheel on a phone for free. */
  flex: none;
  width: 8.5rem;
  min-width: 0;
  font-family: var(--bb-font-italic);
  font-style: italic;
  font-size: 0.9375rem;
  padding: 0.1rem 0.2rem;
}
.turn-date-era { font-family: var(--bb-font-italic); font-style: italic; font-size: 0.9375rem; color: var(--bb-ink-faint); }
.turn-date-echo { font-family: var(--bb-font-italic); font-style: italic; font-size: 0.8125rem; color: var(--bb-ink-faint); }
.turn-date-echo.is-bad { color: var(--bb-vermillion); }
.turn-date-free,
.turn-date-offer {
  background: none; border: none; box-shadow: none; padding: 0; cursor: pointer;
  font-family: var(--bb-font-italic); font-style: italic; font-size: 0.8125rem;
  color: var(--bb-ink-faint); text-decoration: underline dotted; text-underline-offset: 2px;
}
.turn-date-free:hover, .turn-date-offer:hover { color: var(--bb-ink-soft); }
.turn-date-free:focus-visible, .turn-date-offer:focus-visible { outline: 2px solid var(--bb-gilt-deep); outline-offset: 2px; }
/* The free-text arm stacks: the offer is a quiet second line under the box, not
   a competitor beside it. It is the whole discovery path for the feature, so it
   must read as an aside rather than as a choice being demanded. */
.turn-date-edit.is-free { flex-direction: column; align-items: flex-start; gap: 0.15rem; }
/* Progress line — the sweep's rhythm; the gilt tail is the "results await"
   register (good news, never danger). */
.turn-progress { margin: 0.35rem 0 0; font-family: var(--bb-font-display); font-variant: small-caps; letter-spacing: 0.04em; font-size: 0.875rem; color: var(--bb-ink-soft); font-variant-numeric: lining-nums tabular-nums; }
.turn-progress-due { color: var(--bb-gilt-deep); }
/* Maintain (RAW): the whole-sweep verb rides the progress line as a whisper
   ("issue Maintain" / "resume orders"); the gilt prompt below points the
   DM's Bastion-Event roll at the event form — the app records, never rolls. */
.turn-maintain-link { background: none; border: none; padding: 0; margin-left: 0.5rem; cursor: pointer; font-family: var(--bb-font-italic); font-style: italic; font-variant: normal; font-size: 0.875rem; color: var(--bb-ink-faint); text-decoration: underline dotted; text-underline-offset: 2px; letter-spacing: normal; }
.turn-maintain-link:hover { color: var(--bb-ink-soft); }
.turn-maintain-prompt { margin: 0.3rem 0 0; font-family: var(--bb-font-italic); font-style: italic; font-size: 0.875rem; color: var(--bb-gilt-deep); }

/* Sweep cards ride .cap-row.stores-card; only the turn states are new. */
/* The order's level-resolved rules text — same voice as .reg-order-card-body,
   so the page reads like the Register hero it mirrors (and Beyond20 chips
   roll right here). */
.turn-rules { margin: 0 0 0.45rem; font-size: 0.875rem; line-height: 1.45; color: var(--bb-ink-soft); }
.turn-unplaced { font-family: var(--bb-font-italic); font-style: italic; font-size: 0.78rem; color: var(--bb-ink-faint); border: 1px solid var(--bb-rule-soft); padding: 0 6px; }
/* Under construction (#195) — the axis that actually threatens the tracked
   count now; gilt (a "next move"), distinct from the neutral off-map chip. */
.turn-construction { font-family: var(--bb-font-italic); font-style: italic; font-size: 0.78rem; color: var(--bb-gilt-deep); border: 1px solid var(--bb-gilt); padding: 0 6px; }
/* The deliberate no-order-this-week whisper — counts the facility as tended
   and folds its card to a "stood down" row. */
.turn-stand-down { background: none; border: none; padding: 0; cursor: pointer; font-family: var(--bb-font-italic); font-style: italic; font-size: 0.875rem; color: var(--bb-ink-faint); text-decoration: underline dotted; text-underline-offset: 2px; }
.turn-stand-down:hover { color: var(--bb-ink-soft); }
.turn-fac-card.is-busy { opacity: 0.62; }
.turn-fac-card.is-busy .turn-abandon { opacity: 1; }
/* Done gives up the accent edge (#218) — it was verdigris where .is-due is
   gilt, one slot, two states, hue the only difference. The state moved to the
   stamp below; the edge returns to the card's own hairline so nothing is left
   half-signalling. */
.turn-fac-card.is-done { border-left: 1px solid var(--bb-rule-soft); }
/* Due-from-earlier awaiting its result: the gilt act-here accent (the is-done
   border pattern, in the good-news register) — these cards never compact and
   sort to the top of the sweep. */
.turn-fac-card.is-due { border-left: 3px solid var(--bb-gilt); }
.turn-done-mark { margin-left: auto; color: var(--bb-verdigris); font-size: 1.05rem; }

/* THE MUSTER ROLL (#296) — one line per facility, and the page's DEFAULT
   shape. This is the same `.turn-fac-row` grammar that shipped as a fold state
   for handled cards, promoted; the class names are kept deliberately, because
   this is a promotion rather than a new device and a rename would cost a sweep
   `scan_dead_css.py` cannot verify.

   ONE COLUMN TRACK, DECLARED ONCE. `--roll-cols` lives on #turn-page and is
   consumed by both this and the standing strip above it, so `pay it · record`
   lands at the exact x of every `Issue Recruit`. That alignment is the whole
   argument for the strip sitting where it does, and it has no mechanical
   guard: Biome lints no CSS semantics, so two track lists that must stay
   identical would drift the way PHONE_TIER_QUERY can. One declaration removes
   the failure mode instead of watching for it. DO NOT COPY THE TRACK LIST.

   Four areas — glyph, name, detail, act — named rather than positional so the
   phone tier can hang the detail and the act under the name without
   re-stating the columns. The glyph column is wide enough that the strip's
   4px marginal stroke clears the centred glyph.

   Declared AFTER the card state rules so the flat look wins the shared-class
   specificity tie. */
.turn-fac-row,
.turn-standing-row {
  display: grid;
  grid-template-columns: var(--roll-cols);
  grid-template-areas: "g n d a";
  align-items: baseline;
  column-gap: 0.6rem;
  row-gap: 0.15rem;
  border-bottom: 1px dotted var(--bb-rule-soft);
  /* Horizontal padding is on EVERY row, open or closed, so the act column
     stays at one x — measured 614px in both states. It is not cosmetic on the
     open row: with none, the glyph disc (28px in a 24px column) overhung the
     row's own left edge by 2px and the marginal stroke ran under it, and every
     act button and `Abandon` ended flush against the border with nothing
     between them and it. */
  padding: 0.1rem 0.6rem;
}
.turn-fac-row.is-busy { opacity: 1; }
.turn-fac-row.is-done, .turn-fac-row.is-due { border-left: none; }
.turn-fac-row-glyph { grid-area: g; justify-self: center; align-self: start; margin-top: 0.15rem; opacity: 0.55; }
/* The NAME is the disclosure — the row itself is a grid, never a button, so
   the act beside it is not a button inside a button. */
.turn-fac-row-open { grid-area: n; min-width: 0; background: none; border: none; border-radius: 0; box-shadow: none; padding: 0.2rem 0.15rem; cursor: pointer; text-align: left; color: inherit; font: inherit; display: flex; align-items: baseline; gap: 0.4rem; }
.turn-fac-row-open:hover { background: var(--bb-parchment-mid); }
.turn-fac-row-open:focus-visible { outline: 2px solid var(--bb-gilt-deep); outline-offset: 1px; }
.turn-fac-row-name { font-family: var(--bb-font-display); color: var(--bb-ink-soft); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.turn-fac-row-meta { grid-area: d; min-width: 0; font-family: var(--bb-font-display); font-size: 0.875rem; color: var(--bb-ink-soft); font-variant-numeric: lining-nums tabular-nums; }
.turn-fac-row-meta .turn-done-mark { margin-left: 0; font-size: 0.95rem; }
/* The catalog type beside the verb. A custom name ("Grote Zaal") otherwise
   replaces the only place the page says what the facility IS. Set quieter than
   the verb, because the verb is the thing being acted on this week. */
.turn-fac-row-type { font-family: var(--bb-font-italic); font-style: italic; color: var(--bb-ink-faint); }
.turn-fac-row.is-stood .turn-fac-row-meta { font-family: var(--bb-font-italic); font-style: italic; color: var(--bb-ink-faint); }
/* Maintained rows (RAW Maintain) are the one row with no disclosure — a span,
   not a button: no hover wash, no pointer, faint meta. */
.turn-fac-row-static { cursor: default; }
.turn-fac-row-static:hover { background: none; }
.turn-fac-row.is-maintained .turn-fac-row-meta { color: var(--bb-ink-faint); }

/* THE ACT — the third cell, and the reason issuing is one click from an
   unopened page. The CELL stretches so the column holds one x down the roll,
   but the verbs inside it are RIGHT-SET, level with the legends: they are bare
   italic labels rather than bordered buttons, and a ragged left edge on a bare
   label reads as debris where a shared right edge reads as a column.

   (This comment used to claim the verbs STRETCH so their left edges form a
   second vertical rule. They do not — `text-align: right` below has always
   right-set them, and the phone tier's `text-align: left` was the divergence
   rather than the alignment. The claim described bordered buttons that no
   longer exist here.)

   `box-shadow: none` is load-bearing, not tidying — the global button chrome
   paints a pale box behind a bare-text label even with `background: none`
   (the `.linklike` trap). */
.turn-fac-row-act { grid-area: a; justify-self: stretch; min-width: 0; }
.turn-fac-row-act > button { width: 100%; font-size: 0.8125rem; }
.turn-row-legend { display: block; text-align: right; font-family: var(--bb-font-italic); font-style: italic; font-size: 0.8125rem; color: var(--bb-ink-faint); white-space: nowrap; }
.turn-row-open-act,
.turn-row-unstand {
  background: none;
  border: none;
  box-shadow: none;
  border-radius: 0;
  padding: 0.2rem 0;
  cursor: pointer;
  font-family: var(--bb-font-italic);
  font-style: italic;
  font-size: 0.8125rem;
  color: var(--bb-gilt-deep);
  text-align: right;
  text-decoration: underline dotted;
  text-underline-offset: 2px;
}
.turn-row-open-act:hover, .turn-row-unstand:hover { color: var(--bb-ink); }
.turn-row-unstand { color: var(--bb-ink-faint); }
/* No bordered verb button in a CLOSED roll row: the row's act opens the
   facility's week, it never commits an order (see rowActHtml). Every bordered
   control on this page is a commit — the strip's `pay it · record`, and the
   writ's `record` — and they all land in the same column, which is what makes
   the column read as "the verb that writes something down". */

/* ---- Responsive: the advance bar ---------------------------------------- */
/* The seal-and-advance bar goes full width on a phone; Rewind stays a
 * quiet text verb beside it. */
@media (max-width: 700px), (max-height: 500px) and (pointer: coarse) {
  /* --- Turn sheet as a play surface (#158) --- */
  /* Full-width terminal action: the "seal and advance" bar. Rewind stays a
   * text link behind its itemized confirm, so there's no destructive-tap
   * risk in making Advance unmissable. */
  .turn-footer-actions .turn-advance { width: 100%; }
}

/* =========================================================================
 * THE WRIT (#296)
 * ========================================================================= */
/* THERE IS NO CARD. The row being worked unrolls in place: its glyph and its
 * name never move, and the writ is ruled underneath across the detail and act
 * tracks. The record verb therefore lands at the exact x of every act on the
 * roll and of the standing strip's `pay it · record` — the card was the last
 * thing on this page with its own private geometry.
 *
 * Opening costs nothing AND LOOKS LIKE IT COSTS NOTHING: no object appears,
 * the page just makes room to write. That is what makes first-mark issuing
 * legible (see ensureOrder) — a player who opens a row to look has watched
 * nothing be created, so nothing about the page suggests a week was spent.
 *
 * The open row is the only line on the page in the `ink` register rather than
 * `ink-soft`: full-weight name, fuller glyph, a parchment-light ground between
 * two rules with an `ink-soft` marginal. No hue carries any of it. */
.turn-fac-row.is-writing {
  grid-template-areas: "g n d a" ". w w w";
  background: var(--bb-parchment-light);
  border-top: 1px solid var(--bb-rule);
  border-bottom: 1px solid var(--bb-rule);
  /* The marginal stroke is an INSET SHADOW, not a border — measured: a 4px
     border plus the padding it needs moved the act column 3.19px inside the
     closed rows' act cell, which is the one alignment this direction exists to
     make. A shadow costs no layout, so glyph, name, meta and act all stay at
     the x they had when the row was closed. Same reasoning as the standing
     strip's marginal, and the glyph column is wide enough that a 4px stroke
     clears the centred glyph. */
  box-shadow: inset 4px 0 0 var(--bb-ink-soft);
  margin: 0.35rem 0;
  padding: 0.4rem 0.6rem 0.55rem 0.6rem;
  row-gap: 0.1rem;
}
.turn-fac-row.is-writing .turn-fac-row-open { font-size: 1.0625rem; }
.turn-fac-row.is-writing .turn-fac-row-name { color: var(--bb-ink); white-space: normal; }
.turn-fac-row.is-writing .turn-fac-row-glyph { opacity: 0.9; }
.writ-area { grid-area: w; display: grid; grid-template-columns: minmax(0, 1fr) var(--roll-act); column-gap: 0.7rem; align-items: baseline; padding: 0.15rem 0 0; }
.writ-line { grid-column: 1; margin: 0; font-size: 1.0625rem; line-height: 1.8; color: var(--bb-ink); }
/* A ROW, not a block. The cell holds the act and — on an option naming
   something the SRD statted — its lookup glyph. Stacked, the glyph took a line
   of its own under every button (13 of them on a Menagerie); as a flex row the
   button takes the slack and the glyph rides its right edge, which is where it
   belongs: it annotates the option, it is not a second act. `min-width: 0` on
   the button so a long act ellipsizes rather than shoving the glyph out. */
.writ-act { grid-column: 2; justify-self: stretch; display: flex; align-items: center; gap: 0.35rem; }
.writ-act > button { flex: 1 1 auto; min-width: 0; font-size: 0.875rem; }
.writ-act > .ref-glyph { flex: 0 0 auto; }
.writ-cont { display: block; }
.writ-verb-line { display: flex; align-items: baseline; gap: 0.5rem; }
.writ-verb-label { font-family: var(--bb-font-italic); font-style: italic; font-size: 0.875rem; color: var(--bb-ink-soft); }

/* THE RULED BLANK. A blank, not a box: the same "you write here" mark in both
   grammars, so the writ's words and the reckoning line's figure read as one
   page rather than two widgets. `--bb-rule` clears the 3:1 floor a form
   control's border needs in all four themes.

   `min-width: 0` matters — a global 120px floor on inputs beats any `width`
   set here, specificity irrelevant. */
.writ-slot {
  border: none;
  border-bottom: 1px solid var(--bb-rule);
  background: transparent;
  border-radius: 0;
  box-shadow: none;
  vertical-align: baseline;
  font-family: var(--bb-font-italic);
  font-style: italic;
  font-size: 1.0625rem;
  color: var(--bb-ink);
  min-width: 0;
  padding: 0 0.3rem;
}
/* THE AUTHORING LAW, in CSS: a ruled blank ENDS its clause and runs to the
   full measure. A blank set mid-sentence truncates its own placeholder
   mid-word and strands the following punctuation on the next line. */
.writ-slot.is-full { display: block; width: 100%; margin: 0.25rem 0 0.1rem; }
.writ-slot::placeholder { color: var(--bb-ink-faint); }
/* `input:focus` (0,1,1) sets the sunken chrome + a vermillion ring and beats a
   bare `.writ-slot` (0,1,0) — measured on the focused blank, which came back
   carrying both inset shadows. `.writ-slot:focus` (0,2,0) is what wins it. The
   blank thickens its own rule instead: a ruled blank being written in is a
   heavier rule, not a box that appears around the words. */
.writ-slot:focus { outline: none; box-shadow: none; border-color: transparent; border-bottom: 2px solid var(--bb-ink-soft); }
.writ-note { flex: 1 1 12rem; font-size: 0.9375rem; }

/* THE PICKS — what the book offers, one to a line, each ending in the verb
   that journals it. Two columns on the writ's own track, so a page of picks
   has a column of acts rather than a ragged edge. */
.writ-picks { grid-column: 1 / -1; display: grid; grid-template-columns: minmax(0, 1fr) var(--roll-act); column-gap: 0.7rem; row-gap: 0.3rem; margin-top: 0.25rem; align-items: baseline; }
.writ-picks .pick-label { grid-column: 1; font-size: 1.0625rem; color: var(--bb-ink); }
.writ-picks .turn-suggest-apply { grid-column: 2; width: 100%; font-size: 0.875rem; }
.writ-picks .pick-gloss,
.writ-picks .writ-caveat { grid-column: 1; margin: 0; font-family: var(--bb-font-italic); font-style: italic; font-size: 0.84rem; line-height: 1.5; color: var(--bb-ink-faint); }
.writ-caveat { grid-column: 1 / -1 !important; padding-top: 0.15rem; }
/* What every option in the group DOES, said once at the head of the list rather
   than thirteen times across thirteen act buttons. A fact, not a caveat, so it
   is set in the roll's own display face instead of the caveat's italic prose —
   and it sits ABOVE the picks because it qualifies all of them. */
.writ-shared-effect {
  grid-column: 1 / -1;
  margin: 0 0 0.2rem;
  font-family: var(--bb-font-display);
  font-size: 0.8125rem;
  color: var(--bb-ink-faint);
}
.writ-pick-group { grid-column: 1 / -1; font-family: var(--bb-font-display); font-variant: small-caps; letter-spacing: 0.05em; font-size: 0.8125rem; color: var(--bb-ink-soft); }
/* A reckoning line inside a writ lays out on the writ's own track, so its
   `record` joins the column instead of trailing the sentence wherever the
   arithmetic happens to end (measured: 722.69px against the column's 614). */
.turn-fac-row.is-writing .turn-reckon { grid-column: 1 / -1; display: grid; grid-template-columns: minmax(0, 1fr) var(--roll-act); column-gap: 0.7rem; align-items: baseline; }
.turn-fac-row.is-writing .turn-reckon-body { grid-column: 1; }

/* THE FOOT — the workings disclosure and the whispers. The destructive verb
   is set apart on the right, past the span. */
.writ-foot { grid-column: 1 / -1; display: flex; flex-wrap: wrap; align-items: baseline; gap: 0.3rem 0.9rem; margin-top: 0.5rem; padding-top: 0.4rem; border-top: 1px dotted var(--bb-rule-soft); }
.writ-open-workings,
.writ-foot .whisper {
  background: none;
  border: none;
  box-shadow: none;
  border-radius: 0;
  padding: 0;
  cursor: pointer;
  font-family: var(--bb-font-italic);
  font-style: italic;
  font-size: 0.875rem;
  color: var(--bb-ink-faint);
  text-decoration: underline dotted;
  text-underline-offset: 2px;
}
.writ-open-workings { color: var(--bb-ink-soft); }
.writ-open-workings:hover, .writ-foot .whisper:hover { color: var(--bb-ink); }
.writ-foot .whisper.is-red { color: var(--bb-vermillion-deep); margin-left: 0.9rem; }
.writ-foot .whisper-static { font-family: var(--bb-font-italic); font-style: italic; font-size: 0.875rem; color: var(--bb-ink-faint); margin-left: auto; }
.writ-workings { grid-column: 1 / -1; width: 100%; margin-top: 0.4rem; padding-top: 0.4rem; border-top: 1px dotted var(--bb-rule-soft); }
.writ-workings .turn-rules { font-size: 0.9375rem; }
.turn-fac-row.is-writing.is-busy .writ-line { font-family: var(--bb-font-italic); font-style: italic; color: var(--bb-ink-soft); }

/* THE SEALED ROLL — the same grammar, drawn for a week that is over. No act
   cell (there is nothing to do to a sealed week), so the detail spans it; the
   NAME COLUMN still lines up with the live roll's, which is what makes moving
   between weeks read as turning a page rather than loading a different screen.
   That alignment is the reason --roll-cols is declared on #chronicle-turns and
   not on #turn-page — the section owns both surfaces. */
.turn-log-roll { grid-template-areas: ". n d d"; border-bottom: none; padding: 0.05rem 0; }
/* The live name sits inside the disclosure button, so it starts one 0.15rem
   padding in from its cell edge. The sealed name is a bare span and must
   pay the same to land on the same x — measured at 2.39px of drift before
   this line existed. */
.turn-log-roll .turn-fac-row-name { grid-area: n; padding-left: 0.15rem; }
.turn-log-roll .turn-fac-row-meta { grid-area: d; }
.turn-log-roll.is-stood .turn-fac-row-meta,
.turn-log-roll.is-abandoned .turn-fac-row-meta { font-family: var(--bb-font-italic); font-style: italic; color: var(--bb-ink-faint); }
.writ-line.is-stamped { margin: 0 0 0.35rem; font-family: var(--bb-font-italic); font-style: italic; color: var(--bb-ink-soft); }

/* THE DONE STAMP (#218) — completion as an archival act rather than a status
   colour, because the colour could not carry it: verdigris measured 2.13:1 on
   the parchment-mid card ground in verdant, and .is-done/.is-due differed only
   in hue. The stamp gives done an OBJECT the due card lacks, so the two states
   separate in greyscale and at zoom without negotiating the green.

   Deliberately NOT applied to `.turn-fac-row`: the compact row keeps its flat
   `label | meta` grammar and its plain word "done" — a struck box there would
   fight the half-sealed idiom the fold exists to express. */
.writ-line.is-stamped { display: flex; flex-wrap: wrap; align-items: baseline; gap: 0.5rem; }
.writ-line.is-stamped > .dice-prose { flex: 0 1 auto; }

/* ---- Responsive: the roll and the writ at 390px ------------------------- */
/* A MUSTER ROLL IS A RULED LINE, so the phone gives up the columns and
 * keeps the line rather than the reverse. The roll and the writ move as
 * ONE run: `.turn-fac-row.is-writing` is both, so they cannot be split
 * without putting one of them above a base rule it overrides (G6). */
@media (max-width: 700px), (max-height: 500px) and (pointer: coarse) {
  /* A reckoning line (#295) at 390px: the sentence takes the width it needs and
     the tail wraps to its own line, pushed right so the arithmetic and the act
     that commits it sit together at the end of the reading. The blank and its
     unit are already one nowrap group, so the figure never orphans. */
  .turn-reckon-tail { margin-left: auto; }
  /* THE ROLL AT 390px (#296, reshaped) — A MUSTER ROLL IS A RULED LINE, so the
     phone gives up the columns and keeps the line, rather than the reverse.
     Name, a run-out, and the entry made against it; the detail hangs under the
     name ONLY when there is one, which on a fresh turn is never.

     The shape this replaces hung BOTH the detail and the act under the name.
     Its stated reason was sound — a four-column row at 390px shreds — but it
     paid for it with a blank middle line on every un-ordered facility, a third
     line for one italic word, and name/gloss/act all stacked at one x, where a
     thing to read and a thing to tap look identical. Measured on a coarse
     pointer: a closed row 99px -> 50px, twelve of them 1188px -> 597px, the
     whole live page 1926px -> 1231px.

     The strip inherits this for free by sharing --roll-cols and the named
     areas, and it wants the same thing: its act is a bordered commit, its
     detail is long, so the verb sits on the name's line and the account runs
     underneath — the order a ledger writes them in. The two acts no longer
     share a LEFT x at this tier (they are right-set), they share the right
     edge: measured 358px for both. */
  #chronicle-turns { --roll-cols: 1.5rem minmax(0, 1fr) max-content; }
  .turn-fac-row,
  .turn-standing-row {
    grid-template-areas: "g n a" ". d d";
    column-gap: 0.5rem;
    padding: 0.1rem 0.6rem;
  }
  .turn-fac-row.is-writing { grid-template-areas: "g n a" ". d d" ". w w"; }

  /* THE RUN-OUT — the ledger leader `.stores-label` and `.turn-done-leader`
     already speak, and the facing Reckoning table speaks in full. It takes the
     slack between the name and the entry, and it lives INSIDE the disclosure
     button, so the rule is part of the tap target and a long name shortens the
     rule before it ellipsizes itself (floor 1.5rem). */
  .turn-fac-row-open::after {
    content: "";
    flex: 1 1 1.5rem;
    min-width: 1.5rem;
    align-self: center;
    border-bottom: 1px dotted var(--bb-rule-soft);
  }
  /* A leader must end at something. The maintained row is the one row with no
     act at all — under RAW Maintain there is nothing to offer. */
  .turn-fac-row:not(:has(.turn-fac-row-act:not(:empty))) .turn-fac-row-open::after { display: none; }
  /* The open row's writ says all of it below; a run-out to a word repeats it. */
  .turn-fac-row.is-writing .turn-fac-row-open::after { display: none; }

  /* THE ENTRY — right-set at the end of the run-out, in the roll's three tones:
     ink for a row waiting on you, ink-soft for one merely offered, ink-faint
     for one that is settled. Three tones, three words, no hue.

     The link chrome comes off the two acts that only OPEN THE ROW — which is
     what tapping the name already does, so at 390px the underline was
     advertising a second door into the same room, twelve times, and turned a
     written page into a list of links. They stay real controls: the coarse
     44px floor comes off the BOX, which would inflate the row again
     (reference_ref_glyph_coarse_button_trap), and is re-granted as an
     invisible hit-area overlay — the `.ref-glyph` precedent at styles.css:1242.
     The overlay's left reach is MEASURED, not guessed: the shortest act on
     the roll is `Craft…`, and at -0.5rem it hit-tested 43px — one px under
     the floor the overlay exists to grant. It eats the run-out's tail,
     which is the name's slack rather than the name.
     The row is 44px tall regardless, because the name disclosure carries it.

     `undo` keeps its underline. It is the one act on the roll that does
     something the name does not, and being the only underline left is the
     signal. */
  .turn-fac-row-act { justify-self: end; align-self: center; }
  .turn-fac-row-act > .turn-suggest-apply { width: auto; min-height: 44px; }
  .turn-fac-row-act > .turn-row-open-act,
  .turn-fac-row-act > .turn-row-unstand {
    min-height: 0;
    padding: 0.2rem 0;
    text-align: right;
    position: relative;
  }
  .turn-fac-row-act > .turn-row-open-act { color: var(--bb-ink-soft); text-decoration: none; }
  .turn-fac-row.is-due .turn-row-open-act { color: var(--bb-ink); }
  .turn-row-legend { text-align: right; }
  .turn-fac-row-act > .turn-row-open-act::after,
  .turn-fac-row-act > .turn-row-unstand::after {
    content: "";
    position: absolute;
    left: -0.75rem;
    right: 0;
    top: 50%;
    height: 44px;
    transform: translateY(-50%);
  }
  /* is-due carries NO mark in the roll — `.turn-fac-row.is-done, .is-due
     { border-left: none }` drops the card's gilt edge on every tier, so the
     state lived in the act's word and the gloss alone. With the underline gone
     it takes the standing strip's marginal instead: same mark, same meaning (an
     outstanding obligation), same hue family, one weight lighter so the two
     stay tellable, and gilt-deep still carries no text. */
  .turn-fac-row.is-due { box-shadow: inset 3px 0 0 var(--bb-gilt-deep); }
  /* The strip is deliberately NOT sticky: a pinned obligation band eats a
     390px screen, and it is above everything on the page anyway. */
  /* THE WRIT on a phone: one column throughout. The sentence keeps its ruled
     blanks, but every blank goes full width and every act becomes a 44px block
     under the line it commits — the two-column pairing that gives the desktop
     writ its act column has nowhere to go at 390px, and a 10.5rem track beside
     a 1fr one would leave the labels unreadable rather than merely narrow. */
  .writ-area, .writ-picks { grid-template-columns: minmax(0, 1fr); }
  .writ-line { font-size: 1rem; line-height: 2.05; }
  .writ-act, .writ-picks .turn-suggest-apply { grid-column: 1; }
  .writ-act { margin-top: 0.45rem; }
  .writ-act > button, .writ-picks .turn-suggest-apply { min-height: 44px; text-align: left; }
  /* THE RECKONING LINE inside a writ. `.writ-area` and `.writ-picks` already
     drop to one column here; this one was missed and kept the desktop
     `minmax(0,1fr) var(--roll-act)` track — 168px of a 293px area spent on a
     column nothing lands in, the sentence squeezed into 114px and set five
     lines down the left third. Its `record` becomes the same full-width 44px
     block `.writ-act > button` and `.writ-picks .turn-suggest-apply` already
     are. Measured: the line 163px -> 121px, the open writ 369px -> 314px. */
  .turn-fac-row.is-writing .turn-reckon { grid-template-columns: minmax(0, 1fr); }
  .turn-fac-row.is-writing .turn-reckon .turn-suggest-apply {
    grid-column: 1;
    margin-left: 0;
    min-height: 44px;
    text-align: left;
  }
  .writ-slot.is-full { width: 100%; }
  .writ-note { flex: 1 1 100%; }
  /* The destructive verb's separation from the routine whispers does not
     survive the wrap — it lands back at the left on its own line. Recorded
     rather than hidden: at this width there is no room to keep it apart. */
  .writ-foot .whisper-static { margin-left: 0; }
  .writ-foot .whisper.is-red { margin-left: 0; }
  .writ-open-workings, .writ-foot .whisper { min-height: 44px; display: inline-flex; align-items: center; }
  /* The head's acts must wrap, or they hold a narrow right column and shred
     both the date ("Kythorn / 17") and their own labels ("Rewind this / turn"). */
  .turn-page-title-row { flex-wrap: wrap; }
}

/* ---- The done run + sealed-row record ----------------------------------- */
/* Leader and stamp are ONE indivisible run-out. Kept as separate flex children
   of the line they come apart under a long order detail: the leader dead-ends
   at the end of line 1 while the stamp orphans to line 2, flush left. As one
   child the pair either fits beside the sentence or wraps whole, and the leader
   absorbs the slack either way — so a wrapped stamp still lands right, which is
   the phone tier's normal case. */
.turn-done-run { flex: 1 1 9rem; min-width: 9rem; display: flex; align-items: baseline; gap: 0.5rem; }
/* The ledger leader — the dotted run-out that carries the eye from the sentence
   to the legend, the same device `.stores-label` uses. */
.turn-done-leader { flex: 1 1 1.5rem; min-width: 1.5rem; align-self: center; border-bottom: 1px dotted var(--bb-rule); }
/* Struck twice: a 2px plate with a 1px outline standing off it. Rotated a hair
   off true so it reads as applied to the page rather than laid out on it. */
.turn-done-stamp {
  flex: 0 0 auto;
  font-family: var(--bb-font-display);
  font-style: normal;
  font-variant: small-caps;
  font-size: 0.78rem;
  letter-spacing: 0.06em;
  color: var(--bb-ink);
  padding: 1px 8px;
  border: 2px solid var(--bb-ink-soft);
  outline: 1px solid var(--bb-rule-soft);
  outline-offset: 2px;
  transform: rotate(-2.2deg);
}
/* Duration hides behind a quiet dotted link ("takes 1 turn") — the stepper
   appears on demand and folds back on blur. */
.turn-duration-link { background: none; border: none; padding: 0; cursor: pointer; font-family: var(--bb-font-italic); font-style: italic; font-size: 0.875rem; color: var(--bb-ink-soft); text-decoration: underline dotted; text-underline-offset: 2px; white-space: nowrap; }
.turn-duration-link:hover { color: var(--bb-ink); }
.turn-duration-wrap { display: inline-flex; align-items: baseline; gap: 0.3rem; font-family: var(--bb-font-italic); font-style: italic; font-size: 0.875rem; color: var(--bb-ink-soft); white-space: nowrap; }
/* The magnitude tail (#310). The wrap is `nowrap` so the stepper never splits
   across "takes / 36 / turn(s)" — but that also means the tail cannot break,
   and at 390px it overflowed the act cell. Letting the WRAP wrap while its own
   parts stay unbreakable gives the tail its own line on a narrow tier and keeps
   it inline on a wide one, with no media query and no second rule to keep in
   sync with the tier predicate. */
.turn-duration-wrap { flex-wrap: wrap; }
.turn-duration-mag { white-space: normal; color: var(--bb-ink-faint); font-size: 0.8125rem; }
.turn-duration { width: 2.6rem; flex: none; text-align: right; font-family: var(--bb-font-display); font-variant-numeric: lining-nums tabular-nums; padding: 0.1rem 0.3rem; }

/* Record plaques (#188) — the catalog's known costs/yields as one-click
   "record" buttons above the result rows. A BUTTON, never a pre-filled input:
   the gilt edge is the act-here invitation (never vermillion), numerals stay
   ink-on-parchment (gilt fills fail contrast in every theme). Hidden again
   the moment the order carries a row (its group is spoken for). */
.turn-suggest { display: flex; flex-wrap: wrap; align-items: baseline; gap: 0.35rem 0.6rem; margin: 0.1rem 0 0.4rem; }
.turn-suggest-label { font-family: var(--bb-font-display); font-variant: small-caps; font-size: 0.78rem; letter-spacing: 0.04em; color: var(--bb-ink-faint); }
.turn-suggest-apply { font-size: 0.875rem; padding: 3px 10px; background: var(--bb-parchment-light); border: 1px solid var(--bb-rule); border-left: 3px solid var(--bb-gilt); color: var(--bb-ink); cursor: pointer; border-radius: 0; font-variant-numeric: lining-nums tabular-nums; }
.turn-suggest-apply:hover { background: var(--bb-parchment); border-color: var(--bb-ink-soft); border-left-color: var(--bb-gilt-deep); }
.turn-suggest-apply:focus-visible { outline: 2px solid var(--bb-gilt-deep); outline-offset: 1px; }

/* THE WORKINGS (#218) — where a plaque's total came from, set visibly beneath
   the chip instead of in a `data-tip`. Touch has no hover, so the one figure
   the player is asked to trust was the one they could not see; #295 had already
   settled this for the reckoning line. Italic and faint in the
   `.turn-reckon-note` voice — reassurance, never a second control.
   (It reads at 0.84rem on the sunken card ground, which is precisely why
   `--bb-ink-faint` was retuned against `-mid` in the same pass.) */
/* Once ANY chip in a multi-chip row carries a gloss, every chip+gloss pair
   takes its own line: a gloss sitting between two chips reads as belonging to
   either one, which is worse than the tooltip it replaced. */
/* The catalog's own clause — rules prose the option cannot compute, and
   identical across every sibling chip in the group, so it prints ONCE at the
   foot rather than four or five times over. */

/* THE RECKONING LINE (#295) — the SECOND option grammar, and the mismatch with
   the plaques beside it is the point: a chip is the book's figure, a ruled line
   is yours. A set sentence in the rules voice with the arithmetic open and the
   unknown left as an archetype-B ruled blank; the record verb is
   .turn-suggest-apply verbatim, because the committing gesture is identical.
   The computed sum is set IN the line and never in a data-tip, which touch can
   never reach. */
.turn-reckon {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.3rem 0.45rem;
  margin: 0.1rem 0 0.5rem;
  font-family: var(--bb-font-text);
  font-size: 0.9rem;
  color: var(--bb-ink-soft);
}
.turn-reckon-lead { display: inline-flex; align-items: baseline; gap: 0.35rem; flex-wrap: wrap; }
.turn-reckon-dir { min-width: 0; padding: 1px 4px; font-size: 0.85rem; }
/* The blank and its unit are ONE group — split across a line break the figure
   orphans at the end of a line and the sentence stops parsing. */
.turn-reckon-slot { display: inline-flex; align-items: baseline; gap: 0.3rem; white-space: nowrap; }
.turn-reckon-amt.turn-result-num { width: 5rem; min-width: 0; max-width: 5rem; text-align: right; font-size: 0.95rem; }
.turn-reckon-unit { color: var(--bb-ink-soft); }
.turn-reckon-tail { display: inline-flex; align-items: baseline; gap: 0.45rem; flex-wrap: wrap; }
.turn-reckon-note,
.turn-reckon-echo { font-family: var(--bb-font-italic); font-style: italic; color: var(--bb-ink-faint); }
/* Over the bound: the sum steps aside and the line says why, in WORDS — this
   sits where the figure was, so it can't be missed, and it never rests on a
   hue (vermillion is violet in arcane, olive in verdant). */
.turn-reckon-over { font-family: var(--bb-font-display); color: var(--bb-ink); }
.turn-reckon-amt[aria-invalid="true"] { border-bottom-color: var(--bb-vermillion); }
/* The tail's bits are separate clauses, not one phrase — "you bear the cost"
   and "up to 500" run together without a mark between them. */
.turn-reckon-echo::before { content: "· "; }
.turn-reckon-sum { font-family: var(--bb-font-display); color: var(--bb-ink); white-space: nowrap; font-variant-numeric: lining-nums tabular-nums; }
/* The record verb ends the line wherever the line ends — pinned right so it
   never orphans at the left margin reading like a new sentence. */
.turn-reckon .turn-suggest-apply { margin-left: auto; }

/* THE STANDING STRIP (#295, relocated in #296) — every recurring obligation
   running this week, struck ABOVE the roll and outside the order grammar: not
   an order, not an option, not a result. An account book enters its standing
   obligations before the week's transactions, and that is the argument for
   where this sits.

   It shares `--roll-cols` with the roll rather than restating it, so its act
   cell lands at the exact x of the roll's — see the roll block above for why
   that is one declaration and not two.

   Answered vs ignored reads in the TENSE, never in a hue: verdigris falls to
   2.76:1 in verdant, so a state carried by colour is a state some readers
   never get. */
.turn-standing-strip {
  margin: 0.1rem 0 0.9rem;
  padding: 0.2rem 0 0.1rem;
  border-top: 1px solid var(--bb-rule-soft);
  border-bottom: 1px solid var(--bb-rule-soft);
}
.turn-standing-caption {
  margin: 0 0 0.2rem;
  font-family: var(--bb-font-italic);
  font-style: italic;
  font-size: 0.8125rem;
  color: var(--bb-ink-faint);
}
.turn-standing-row:last-child { border-bottom: none; }
/* The marginal stroke is an inset SHADOW, not a border: a border would push
   every strip row 4px right of the roll's rows and break the alignment the
   strip exists to have. It also deliberately does NOT reuse the roll's is-due
   gilt FILL — three gilt-filled rows stack and read as one group, so a routine
   payment would out-weight ten facilities of orders, a hierarchy error the
   round-4 render settled. Same hue family, different weight.

   gilt-deep measures 5.45 default / 3.79 verdant: above the 3:1 graphic floor
   in both, BELOW AA for text in verdant. IT MAY CARRY THE STROKE, NEVER A
   WORD. */
.turn-standing-row.is-outstanding { box-shadow: inset 4px 0 0 var(--bb-gilt-deep); }
.turn-standing-row .turn-fac-row-meta strong { font-family: var(--bb-font-display); color: var(--bb-ink); white-space: nowrap; }
.turn-standing-past { display: block; font-family: var(--bb-font-italic); font-style: italic; color: var(--bb-ink-faint); }
/* Settled goes past tense and gives up its stroke and its verb — but NOT its
   row. Presence-vs-absence cannot be told from "no War Room" or "the app
   forgot"; a ledger prints its standing lines whether or not they are struck.
   The alarm channel clears separately, in the pre-advance note. */
.turn-standing-row.is-settled .turn-fac-row-meta,
.turn-standing-row.is-settled .turn-fac-row-name {
  font-family: var(--bb-font-italic);
  font-style: italic;
  color: var(--bb-ink-faint);
}
/* The row's own disclosure: the fuller history, and the destructive second
   act. Standing down zeroes the guards, so it is not co-equal with paying
   (paying is overwhelmingly the answer) and must not be pre-armed beside it as
   a picker. It spans the whole grid beneath the row. */
.turn-standing-detail {
  grid-column: 1 / -1;
  margin: 0.15rem 0 0.35rem;
  padding-left: 0.6rem;
  border-left: 1px solid var(--bb-rule-soft);
}
.turn-standing-history {
  margin: 0 0 0.3rem;
  font-family: var(--bb-font-italic);
  font-style: italic;
  font-size: 0.8125rem;
  color: var(--bb-ink-faint);
}

/* Result rows — signed, user-entered; gp | qty | item, the way a result is
   said aloud. Header and rows share one grid so the labels stay glued to
   their columns; min-width:0 everywhere or the number inputs' intrinsic
   width blows the grid out the card's right edge (the round-3 screenshot). */
.turn-results { margin: 0.2rem 0 0.4rem; }
.turn-results-head,
.turn-result-row { display: grid; grid-template-columns: 4.25rem 3rem minmax(0, 1fr) 1.25rem; gap: 0.4rem; align-items: center; }
.turn-results-head > span,
.turn-result-row > * { min-width: 0; }
.turn-results-head { margin-bottom: 0.15rem; }
.turn-results-head span { font-family: var(--bb-font-display); font-variant: small-caps; font-size: 0.72rem; letter-spacing: 0.04em; color: var(--bb-ink-faint); }
.turn-results-head span:nth-child(1),
.turn-results-head span:nth-child(2) { text-align: right; }
.turn-result-row { margin-bottom: 0.3rem; }
.turn-result-num { text-align: right; font-family: var(--bb-font-display); font-size: 0.95rem; font-variant-numeric: lining-nums tabular-nums; padding: 0.15rem 0.3rem; width: 100%; }
.turn-result-item { font-family: var(--bb-font-italic); font-size: 0.9375rem; padding: 0.15rem 0.4rem; width: 100%; }
.turn-result-del { background: none; border: none; color: var(--bb-ink-faint); cursor: pointer; font-size: 1.1rem; line-height: 1; padding: 0 0.2rem; }
.turn-result-del:hover { color: var(--bb-danger); }
.turn-result-add { background: none; border: none; padding: 0; cursor: pointer; font-family: var(--bb-font-italic); font-style: italic; font-size: 0.9rem; color: var(--bb-ink-soft); text-decoration: underline dotted; text-underline-offset: 2px; }
.turn-result-add:hover { color: var(--bb-ink); }


/* ---- Send verbs: VTT, Discord, share (#199/#209) ------------------------ */
/* Shared bare vermillion text-link (Abandon, Rewind) — the char-focus-btn
   vocabulary: quiet until hovered, never a filled danger button. */
.link-vermillion { background: none; border: none; box-shadow: none; padding: 0; cursor: pointer; font-family: var(--bb-font-italic); font-style: italic; font-size: 0.9rem; color: var(--bb-vermillion); text-decoration: underline dotted; text-underline-offset: 2px; }
.link-vermillion:hover { color: var(--bb-vermillion-deep); }
.link-vermillion:focus-visible { outline: 2px solid var(--bb-vermillion); outline-offset: 1px; }

/* Send to VTT (#198) — the shared Beyond20 share control: the gilt cousin of
   .link-vermillion / .fig-turn-link (gilt = an available reference action;
   vermillion stays destructive). Rendered ONLY while Beyond20 is present —
   the #145 degrade contract, so never a disabled stub. What varies per
   surface is seating only, never the look. */
.vtt-send, .discord-send, .share-send, .share-file { background: none; border: none; box-shadow: none; padding: 0; cursor: pointer; font-family: var(--bb-font-italic); font-style: italic; font-size: 0.8125rem; letter-spacing: normal; text-transform: none; color: var(--bb-gilt-deep); text-decoration: underline dotted; text-underline-offset: 2px; white-space: nowrap; }
.vtt-send:hover, .discord-send:hover, .share-send:hover, .share-file:hover { color: var(--bb-ink); }
.vtt-send:focus-visible, .discord-send:focus-visible, .share-send:focus-visible, .share-file:focus-visible { outline: 2px solid var(--bb-gilt); outline-offset: 1px; }
/* Charter circulation cluster (#271) — the whole-bastion verbs that put this
   bastion in front of someone else. A wrapping ROW, never a stack: at the coarse
   tier the shared 44px button floor would otherwise stack 130px of chrome above
   the figures. `:empty` matters because .share-send is the only unconditional
   member — with Beyond20 absent and the cluster somehow emptied, this collapses
   rather than leaving a gap under the owner line (#145: no dead controls, and no
   dead containers either). */
.charter-acts { display: flex; flex-wrap: wrap; gap: 6px 16px; margin-top: 8px; }
.charter-acts:empty { display: none; }
/* A snapshot has no saved record to build a link from, so the verb would only
   ever toast "Couldn't find that bastion to share." The two dispatch verbs stay:
   canPost() already degrades them to a plain copy, and copying a summary of
   somebody else's snapshot is harmless. */
body.viewer #charter-share-link { display: none; }
/* .discord-send (#199) is the same gilt verb voice but its own class: .vtt-send
   visibility rides the Beyond20 handshake, Discord's never does (the copy
   floor always works) — sharing a class would conflate the two gates. */
/* Seating: the Register full-rules disclosure (the payload IS that text; the
   summary keeps its list-item marker, so the link floats right), Household
   card heads (identity anchor left, forward action right — vacant hirelings
   have nothing to forward), Charter identity head, the Stores section head,
   the turn title row's right cluster (gilt left of vermillion Rewind), and
   the milestone footer's action pair. */
.reg-full-rules > summary .vtt-send { float: right; }
.house-card-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
.house-card-head-trail { display: flex; align-items: center; gap: 8px; margin-left: auto; }
/* Under-construction annotation (#195) — the roster keeps showing a staffed
   facility's card (an already-named hireling shouldn't vanish), but flags
   that it isn't counted toward capacity right now. Same gilt "next move"
   treatment as the Register strip, scaled down for a card-head chip. */
.house-construction-chip {
  flex: none;
  padding: 0 5px;
  font-family: var(--bb-font-display);
  font-size: 10px;
  letter-spacing: 0.04em;
  border: 1px solid var(--bb-gilt);
  border-radius: 2px;
  color: var(--bb-gilt-deep);
  white-space: nowrap;
}
.house-recruit-head .vtt-send { margin-left: auto; }
#charter-vtt-send { margin-top: 2px; }
#charter-discord-send { margin-top: 2px; }
/* A sealed week's recap verb sits right-aligned at the foot of the week's page
   — the last thing after the roll, the way a recap is the last thing you do
   with a week. The live page never grows this verb. */
.turn-log-discord { display: block; margin-left: auto; margin-top: 0.6rem; }

/* Discord post preview (#199): literal markdown source inside the post
   choiceModal — never a mock of Discord's rendered chrome. Scroll-gutter
   convention: right padding + stable gutter. */
.discord-preview {
  max-height: 40vh;
  overflow: auto;
  padding: 8px;
  padding-right: 12px;
  scrollbar-gutter: stable;
  background: var(--bb-parchment-light);
  border: 1px solid var(--bb-hairline);
  border-radius: 3px;
  font-family: ui-monospace, "Cascadia Mono", Consolas, monospace;
  font-size: 0.76rem;
  line-height: 1.45;
  white-space: pre-wrap;
  word-break: break-word;
  color: var(--bb-ink);
  text-align: left;
}

/* Settings › At the table › Discord channels (#199): one row per connected
   bastion, bastion name as the anchor, masked URL, Forget. Orphaned entries
   (bastion deleted out from under an older build's connection) mute the name
   but keep Forget — a credential the user set is never silently discarded. */
.settings-subtitle {
  margin: 0.8rem 0 0.1rem;
  font-family: var(--bb-font-display);
  font-size: 0.85rem;
  font-variant: small-caps;
  letter-spacing: 0.05em;
  color: var(--bb-ink-soft);
}
/* Connect dialog fields ride the .hb-field labeled-stack convention (label
   above, input, hint below, full width) — .settings-row's grid is for the
   Settings toggles and crams the hint into a right column here. */
#discord-connect-dialog .hb-field { margin: 0.7rem 0; }
#discord-connect-dialog .hb-field input { width: 100%; }

.discord-channel-row { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
.discord-channel-info { display: flex; flex-direction: column; min-width: 0; }
.discord-channel-name { font-family: var(--bb-font-display); font-size: 0.85rem; color: var(--bb-ink); }
.discord-channel-row.is-orphan .discord-channel-name { color: var(--bb-ink-faint); font-style: italic; }
/* Share dialog (#209): the secondary-verb row beneath the Copy link row, home
   to the quiet-gilt Send to VTT. Wraps on phones; the thin verb gets a
   comfortable tap target at the narrow tier. */
.share-more-acts { display: flex; flex-wrap: wrap; align-items: baseline; gap: 0.4rem 1rem; margin-top: 0.5rem; }
/* Send to VTT pins right; #272's Save as a file takes the left of the row. By
   ID, not `.vtt-send`, because two auto margins in one flex row push BOTH
   members rightward and strand the first one mid-line. */
.share-more-acts #share-vtt-send { margin-left: auto; }
/* tier — tap target (#283) */
@media (max-width: 700px), (max-height: 500px) and (pointer: coarse) { .share-more-acts > * { padding: 0.5rem 0; } }
.turn-page-title-row .vtt-send { margin-left: auto; }
.milestone-footer-acts { display: inline-flex; align-items: baseline; gap: 0.7rem; }


/* ---- Turn trackers ------------------------------------------------------ */
/* On-card Stores counters — the "update the stores" step of the per-facility
   flow. The readout owns the count ("12 / 20"); the small ± field records the
   week's signed change (the Stores card below sets totals). */
.turn-trackers { margin: 0.2rem 0 0.4rem; padding-top: 0.35rem; border-top: 1px dotted var(--bb-rule-soft); }
.turn-tracker-row { display: grid; grid-template-columns: 1fr auto; gap: 0.35rem 0.5rem; align-items: baseline; margin-bottom: 0.25rem; }
.turn-tracker-label { font-family: var(--bb-font-display); font-variant: small-caps; font-size: 0.8rem; color: var(--bb-ink-faint); letter-spacing: 0.04em; }
.turn-tracker-field { display: inline-flex; align-items: baseline; gap: 0.3rem; justify-self: end; }
.turn-tracker-count { font-family: var(--bb-font-display); font-size: 0.9rem; color: var(--bb-ink); font-variant-numeric: lining-nums tabular-nums; }
.turn-tracker-delta { width: 3rem; flex: none; }

/* Chronicle log: per-order recorded results, quiet next to the order line. */

/* On-card arrivals ("+ someone arrived") — journaled recruits, recorded where
   the Recruit order lives. */
.turn-arrivals { margin: 0.2rem 0 0.1rem; }
.turn-arrivals-line { margin: 0 0 0.25rem; font-family: var(--bb-font-italic); font-style: italic; font-size: 0.875rem; color: var(--bb-ink-soft); }
/* Four tracks since #218 added the "and count them" tick; a form without it has
   three children and simply leaves the last track empty. */
.turn-arrival-form { display: grid; grid-template-columns: minmax(0, 1fr) auto auto auto; gap: 0.4rem; align-items: center; margin-bottom: 0.25rem; }
/* The tick that says this arrival is a NEW body rather than a name for one
   already in the tally. Ticked by default — a hand-added arrival at a housing
   facility is nearly always the former. */
.turn-arrival-count { display: inline-flex; align-items: center; gap: 0.3rem; font-family: var(--bb-font-italic); font-style: italic; font-size: 0.8125rem; color: var(--bb-ink-soft); white-space: nowrap; cursor: pointer; }
.turn-arrival-count input { min-width: 0; margin: 0; }
.turn-arrival-name-wrap { position: relative; display: block; min-width: 0; }
.turn-arrival-name { min-width: 0; width: 100%; font-family: var(--bb-font-italic); font-size: 0.9375rem; padding: 0.15rem 1.35rem 0.15rem 0.4rem; }
.turn-arrival-kind { font-size: 0.875rem; color: var(--bb-ink-soft); }
.turn-arrival-commit { font-size: 0.84rem; padding: 2px 10px; background: var(--bb-parchment-light); border: 1px solid var(--bb-rule); color: var(--bb-ink); cursor: pointer; border-radius: 0; }
.turn-arrival-commit:hover { background: var(--bb-parchment); border-color: var(--bb-ink-soft); }
.turn-arrival-link { background: none; border: none; padding: 0; cursor: pointer; font-family: var(--bb-font-italic); font-style: italic; font-size: 0.9rem; color: var(--bb-ink-soft); text-decoration: underline dotted; text-underline-offset: 2px; }
.turn-arrival-link:hover { color: var(--bb-ink); }


/* Event outcomes — "the table already rolled it; note what changed." The
   dotted top rule marks the zone off from the sweep (on the flat page the
   trailing position no longer does that for free); the measure cap keeps the
   delta/gold rows at a comfortable form width on the 760px column. */
.turn-event { margin: 0.9rem 0 0.4rem; padding-top: 0.6rem; border-top: 1px dotted var(--bb-rule-soft); }
.turn-event > summary { cursor: pointer; font-family: var(--bb-font-italic); font-style: italic; color: var(--bb-ink-soft); }
.turn-event > summary:hover { color: var(--bb-ink); }
.turn-event-form { padding: 0.6rem 0 0.2rem; display: flex; flex-direction: column; gap: 0.6rem; max-width: 36rem; }
.turn-event-sub { margin: 0; font-size: 0.875rem; color: var(--bb-ink-faint); }
.turn-event-label { font-family: var(--bb-font-italic); font-size: 0.9375rem; padding: 0.25rem 0.4rem; }
.turn-event-group-label { display: block; font-family: var(--bb-font-display); font-variant: small-caps; font-size: 0.8rem; letter-spacing: 0.04em; color: var(--bb-ink-faint); margin-bottom: 0.25rem; }
.turn-delta-row { display: grid; grid-template-columns: 1.4fr 1fr 4rem 1.5rem; gap: 0.4rem; align-items: center; margin-bottom: 0.3rem; }
.turn-recruit-row { display: grid; grid-template-columns: 1.2fr auto 1fr 1.5rem; gap: 0.4rem; align-items: center; margin-bottom: 0.3rem; }
/* Gold rows share the sweep cards' result-row grammar byte-for-byte
   (± gp | ± qty | item under the same header) — same grid, same order. */
.turn-gold-row { display: grid; grid-template-columns: 4.25rem 3rem minmax(0, 1fr) 1.25rem; gap: 0.4rem; align-items: center; margin-bottom: 0.3rem; }
/* min-width:0 or the number inputs' intrinsic width blows past their fixed
   columns and slides UNDER the next field — right-aligned typed digits paint
   beneath the neighbour's opaque background, so the field looks dead (the
   round-3 .turn-result-row lesson, belatedly applied to the event form). */
.turn-delta-row > *, .turn-recruit-row > *, .turn-gold-row > * { min-width: 0; }
/* Condition offer row (#250): the verb+duration label, the facility picker, and
   the remove ×. The label leads (it's the fixed part); the facility ▾ is the
   only field the user sets. */
.turn-condition-row { display: grid; grid-template-columns: auto 1fr 1.5rem; gap: 0.4rem; align-items: center; margin-bottom: 0.3rem; }
.turn-condition-row > * { min-width: 0; }
.turn-condition-verb { font-family: var(--bb-font-display); font-variant: small-caps; font-size: 0.8rem; letter-spacing: 0.03em; color: var(--bb-vermillion-deep); white-space: nowrap; }
/* #208: the name field is wrapped so the suggest quill can anchor to its
   trailing edge; the wrap is the grid child now, the input fills it. */
.turn-recruit-name-wrap { position: relative; display: block; min-width: 0; }
.turn-recruit-name { font-family: var(--bb-font-italic); font-size: 0.9375rem; padding: 0.15rem 1.35rem 0.15rem 0.4rem; width: 100%; }
.turn-gold-item { font-family: var(--bb-font-italic); font-size: 0.9375rem; padding: 0.15rem 0.4rem; width: 100%; }
.turn-row-del { background: none; border: none; color: var(--bb-ink-faint); cursor: pointer; font-size: 1.1rem; line-height: 1; padding: 0 0.2rem; }
.turn-row-del:hover { color: var(--bb-danger); }
.turn-event-add { background: none; border: none; padding: 0; cursor: pointer; font-family: var(--bb-font-italic); font-style: italic; font-size: 0.9rem; color: var(--bb-ink-soft); text-decoration: underline dotted; text-underline-offset: 2px; }
.turn-event-add:hover { color: var(--bb-ink); }
.turn-event-actions { display: flex; justify-content: flex-end; }
.turn-event-record { font-size: 0.875rem; padding: 3px 12px; background: var(--bb-parchment-light); border: 1px solid var(--bb-rule); color: var(--bb-ink); cursor: pointer; border-radius: 0; }
.turn-event-record:hover { background: var(--bb-parchment); border-color: var(--bb-ink-soft); }

/* Bastion Events reference (#200) — die-then-reading: the narrow roll input
   maps the DM's result to the table entry; the select is the same lookup by
   name. One composed control, so the block strides tighter than the form. */
.turn-event-pick-block { display: flex; flex-direction: column; gap: 0.35rem; align-items: flex-start; }
.turn-event-pick-row { display: flex; align-items: center; gap: 0.4rem; width: 100%; }
.turn-event-roll { width: 2.9rem; flex: none; text-align: right; font-family: var(--bb-font-display); font-size: 0.95rem; font-variant-numeric: lining-nums tabular-nums; padding: 0.15rem 0.3rem; }
/* The die stamp fronts the row (roll-die-chip look, its prose margin
   dropped); rolling it is the send-to-VTT — the input stays the record. */
.turn-event-pick-row .roll-die-chip { flex: none; margin-left: 0; font-size: 0.8rem; }
.turn-event-pick { min-width: 0; flex: 1; }
.turn-event-hint { margin: 0; font-family: var(--bb-font-italic); font-style: italic; font-size: 0.875rem; color: var(--bb-ink-faint); }
/* Attack casualty helper (#251) — the one COMPUTED line in the event form. No
   panel, no spine (both spine semantics are spoken for): emphasis is the pool
   line's ink weight, and adverse valence is carried in vermillion text only. */
.turn-casualty { display: flex; flex-direction: column; gap: 0.3rem; margin: 0.15rem 0 0.1rem; }
.turn-casualty-pool { margin: 0; color: var(--bb-ink); font-size: 0.9375rem; }
.turn-casualty-why { font-family: var(--bb-font-italic); font-style: italic; color: var(--bb-ink-faint); font-size: 0.82rem; }
.turn-casualty-tally-row { display: flex; align-items: baseline; gap: 0.5rem; flex-wrap: wrap; }
.turn-casualty-label { display: inline-flex; align-items: baseline; gap: 0.4rem; font-family: var(--bb-font-display); font-variant: small-caps; font-size: 0.8rem; letter-spacing: 0.04em; color: var(--bb-ink-faint); }
.turn-casualty-ones { width: 3rem; flex: none; }
.turn-casualty-readout { font-family: var(--bb-font-italic); font-style: italic; font-size: 0.875rem; color: var(--bb-vermillion-deep); }
.turn-casualty-readout.is-complete { color: var(--bb-gilt-deep); }
.turn-casualty-alarm { margin: 0; font-family: var(--bb-font-italic); font-style: italic; font-size: 0.875rem; color: var(--bb-vermillion-deep); }
/* Who is standing at the posts a casualty roll just thinned (#218). Ink, not
   vermillion: this is information, not an alarm — the app is naming the people
   it CANNOT choose between, deliberately stopping short of deciding. */
.turn-casualty-named { margin: 0.2rem 0 0; font-family: var(--bb-font-italic); font-style: italic; font-size: 0.8125rem; color: var(--bb-ink-soft); }
/* The gilt prompts' own actions (record the outcome / all is well / record a
   haunting) — links in the prompt's italic voice, never separate buttons. */
.turn-prompt-record, .turn-quick-well { background: none; border: none; padding: 0; cursor: pointer; font: inherit; color: var(--bb-gilt-deep); text-decoration: underline dotted; text-underline-offset: 2px; }
.turn-prompt-record:hover, .turn-quick-well:hover { color: var(--bb-ink); }
.turn-prompt-record:focus-visible, .turn-quick-well:focus-visible { outline: 2px solid var(--bb-gilt-deep); outline-offset: 1px; }

/* Footer: the page's closing line — derived tally (sign-neutral ink — an
   almanac, not a P&L), the honest pre-advance note when work remains, and the
   loop's primary verb. Rewind lives in the page head, away from Advance. */
.turn-page-footer { margin-top: 0.9rem; padding-top: 0.7rem; border-top: 1px solid var(--bb-rule-soft); }
.turn-advance-note { margin: 0 0 0.5rem; font-family: var(--bb-font-italic); font-style: italic; font-size: 0.875rem; color: var(--bb-gilt-deep); }
/* Active-condition reminders on an expanded sweep card (#250) — the
   .turn-advance-note idiom, valence-coloured. Enforcing conditions name their
   cause in the folded row's meta; this is the informational (double / slowed)
   reminder, and what an enforcing card shows once reopened to override. */
.turn-condition-notes { display: flex; flex-wrap: wrap; gap: 2px 10px; margin: 0.1rem 0 0.4rem; }
.turn-condition-note { font-family: var(--bb-font-italic); font-style: italic; font-size: 0.82rem; }
.turn-condition-note.is-adverse { color: var(--bb-vermillion-deep); }
.turn-condition-note.is-boon { color: var(--bb-gilt-deep); }
.turn-tally { margin: 0 0 0.6rem; font-family: var(--bb-font-display); font-size: 0.95rem; color: var(--bb-ink); font-variant-numeric: lining-nums tabular-nums; }
.turn-footer-actions { display: flex; justify-content: flex-end; align-items: baseline; gap: 1rem; }

/* "Earlier turns" — the ruled divider between the open page and the sealed
   log rows (hidden until a second turn exists). */
.turn-log-divider { margin: 1.4rem 0 0.4rem; padding-bottom: 0.25rem; border-bottom: 1px solid var(--bb-rule-soft); font-family: var(--bb-font-display); font-variant: small-caps; font-weight: normal; letter-spacing: 0.05em; font-size: 0.9rem; color: var(--bb-ink-faint); }

/* Chronicle › The Bastion Turns: the Begin invitation (gilt = gentle, never a
   vermillion alarm) and the read-only turn log — the handout ledger's
   label | meta grammar transposed to app tokens. */
.btn-begin-turns { align-self: flex-start; background: var(--bb-parchment-light); border: 1px solid var(--bb-gilt); color: var(--bb-gilt-deep); font-family: var(--bb-font-display); font-size: 0.95rem; padding: 6px 14px; cursor: pointer; border-radius: 0; }
.btn-begin-turns:hover { background: color-mix(in srgb, var(--bb-gilt) 12%, var(--bb-parchment-light)); border-color: var(--bb-gilt-deep); }
/* `.turn-log-meta` survives the sealed log's retirement (#296) — it is the
   quiet right-hand figure grammar, and the milestone plates borrow it. */
.turn-log-meta { font-family: var(--bb-font-display); font-size: 0.875rem; color: var(--bb-ink-soft); font-variant-numeric: lining-nums tabular-nums; white-space: nowrap; }
.turn-log-note { font-family: var(--bb-font-italic); font-style: italic; color: var(--bb-ink-soft); }
.turn-log-quiet { margin: 0.6rem 0 0.15rem; font-family: var(--bb-font-italic); font-style: italic; font-size: 0.9rem; color: var(--bb-ink-faint); }


/* ---- The fore-edge index (#296) ----------------------------------------- */
/* THE FORE-EDGE (#296) — the sealed log promoted from content to NAVIGATION.
   Before this it was one expandable row per week, growing forever below the
   live page: ten turns measured 315px with no n at which it stopped. As a
   thumb index it costs the same margin at every n, which is what caps the
   chapter's growth and what lets a sealed week afford a whole facing page.

   THE STRIP IS THE BASE SHAPE, THE MARGIN IS THE SPREAD'S. Written this way
   round on purpose: the horizontal strip needs no breakpoint of its own (it is
   simply what a row of tabs above a page looks like), and the vertical margin
   only exists inside the ≥1200px spread block where there IS a margin to put
   it in. Writing it the other way would need a max-width query mirroring the
   spread's min-width — two numbers that must agree, with nothing to check
   them, which is the PHONE_TIER_QUERY failure mode.

   The tabs' own edges draw the index's rule, rather than a border on the nav:
   the line is then exactly as deep as there are weeks, and never runs on over
   blank parchment. */
.chronicle-foredge {
  display: flex;
  flex-direction: row;
  gap: 0;
  overflow-x: auto;
  scrollbar-gutter: stable;
  border-bottom: 1px solid var(--bb-rule-soft);
  /* #310: once the tabs carry dates the strip no longer fits. Measured at
     390px: today's blank labels are 346px exactly — the strip has never
     scrolled — and month-elided dates are 606px. Reducing FOREDGE_VISIBLE on
     the phone tier would not avoid the change, it would merely also delete
     three weeks of history from the index permanently, so the strip scrolls and
     says so: snap points make it feel like turning pages, and the fade tells
     the reader there is more index off the edge. */
  scroll-snap-type: x proximity;
  mask-image: linear-gradient(to right, #000 calc(100% - 24px), transparent 100%);
}
.chronicle-foredge > * { scroll-snap-align: start; }
.foredge-tab,
.foredge-more {
  flex: 0 0 auto;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0;
  background: none;
  border: none;
  border-radius: 0;
  box-shadow: none;
  border-bottom: 2px solid transparent;
  padding: 0.3rem 0.6rem;
  cursor: pointer;
  color: inherit;
  text-align: center;
}
.foredge-n { font-family: var(--bb-font-display); font-size: 1.05rem; line-height: 1.1; color: var(--bb-ink-soft); font-variant-numeric: lining-nums; }
.foredge-lab { font-family: var(--bb-font-italic); font-style: italic; font-size: 0.72rem; line-height: 1.2; color: var(--bb-ink-faint); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 100%; }
.foredge-tab:hover, .foredge-more:hover { background: var(--bb-parchment-mid); }
.foredge-tab:focus-visible, .foredge-more:focus-visible { outline: 2px solid var(--bb-gilt-deep); outline-offset: -2px; }
/* The live week's numeral is vermillion, matching its own versal on the page —
   the one week that is happening now, marked the same way in both places. */
.foredge-tab.is-live .foredge-n { color: var(--bb-vermillion); }
/* WHICH PAGE YOU ARE ON is carried by the edge, not by the numeral's colour:
   the live tab is already vermillion, so colouring the open tab would leave
   "live" and "open" indistinguishable on the page where they coincide — which
   is the page you are on nine times out of ten. */
.foredge-tab.is-open { border-bottom-color: var(--bb-vermillion); background: var(--bb-parchment-mid); }
.foredge-tab.is-open .foredge-n { color: var(--bb-ink); }
.foredge-tab.is-open.is-live .foredge-n { color: var(--bb-vermillion); }
.foredge-more { font-family: var(--bb-font-italic); font-style: italic; font-size: 0.75rem; color: var(--bb-ink-faint); justify-content: center; }
/* THE WORDS (#312) — a written week's opening words under its label, in a
   DIFFERENT HAND: small upright roman in the soft ink, where the label keeps
   the italic. In one italic hand "the 5th / Session 4" read as a date and a
   title; in two the words are a footnote to the label. Soft ink, not faint:
   faint roman at this size sat on the contrast floor at true 1x.
   THE CUT IS THE SCRIPT'S, NOT THE CLAMP'S: the span is empty and carries two
   word-honest cuts (render-history.js `excerpt`) as `data-strip` / `data-
   margin`, and `content: attr()` picks one per tier — the strip's one line
   here, the margin's two lines in the spread block. A CSS clamp cut mid-word
   and, at two lines, dropped a third silently. */
.fe-words { display: block; margin-top: 3px; max-width: 104px; font-family: var(--bb-font-text); font-style: normal; font-size: 0.66rem; line-height: 1.2; color: var(--bb-ink-soft); white-space: nowrap; overflow: hidden; }
.fe-words::before { content: attr(data-strip); }
/* THE GATHERING MARK (#310) — "the story jumped here". A doubled hairline in
   the index's own rule, perpendicular to the run, with no colour and no words:
   it is a mark a reader notices while scanning rather than a badge that
   announces itself. It rides the NEWER week (whose `days_since` records the
   jump) on the edge facing its older neighbour — which, because the index is
   newest-first, is the tab's RIGHT in the strip and its BOTTOM in the margin.
   Threshold is 2x the declared cadence, not "longer than the cadence": an
   8-day week on a roughly-weekly table is the common case, and a mark there
   would be noise from day one. */
.foredge-tab { position: relative; }
.foredge-tab.is-gathering::after {
  content: "";
  position: absolute;
  right: -2px;
  top: 22%;
  bottom: 22%;
  width: 3px;
  border-left: 1px solid var(--bb-rule-soft);
  border-right: 1px solid var(--bb-rule-soft);
  pointer-events: none;
}

/* THE STANDING RAIL (#311) — see renderForeEdge's header for the decisions.
   In the strip it rules ACROSS the top of the index: the nav's padding-top is
   the room it needs, and it is load-bearing rather than spacing — the strip's
   `overflow-x: auto` forces its overflow-y to auto as well, so a mark drawn
   above the tab box is clipped (or grows a scrollbar) without it. Each tab
   rules its own segment of the line (a `::before`), the fold marker included,
   so the line is exactly as long as the index. */
.chronicle-foredge { position: relative; }
.chronicle-foredge.has-rail { padding-top: 12px; }
.foredge-more { position: relative; }
.has-rail .foredge-tab::before,
.has-rail .foredge-more::before,
.fe-founded::before {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  top: -8px;
  height: 1px;
  background: var(--bb-rule-soft);
  pointer-events: none;
}
/* The lozenge. An <i>, deliberately not a button: the coarse-pointer floor
   (`button { min-height: 44px }`) would inflate it and burst the index's
   rhythm; as a child of the tab it inherits the tab's target, which already
   carries that floor. NOT `pointer-events: none` — hit-testing follows the
   element tree only for an element that accepts pointer events; with `none`
   the point at the mark's centre fell past the tab's border box to the grid
   container (measured). Live, the mark is hit, its ancestor is the tab, and
   the click bubbles to the tab's handler: the mark IS the tab. */
.fe-mark {
  position: absolute;
  z-index: 1;
  left: 50%;
  margin-left: -3.5px;
  top: -11.5px;
  width: 7px;
  height: 7px;
  background: var(--bb-gilt);
  transform: rotate(45deg);
}
/* The fold's segment: the rail thickened across the marker's span, saying
   "marks in here" where the index says "weeks in here". */
.foredge-more .fe-seg {
  position: absolute;
  z-index: 1;
  left: 6px;
  right: 6px;
  top: -10.5px;
  height: 5px;
  background: var(--bb-gilt);
  border-radius: 1px;
}
/* The mark answers the tab's own states — which is what says it is the tab. */
.foredge-tab:hover .fe-mark,
.foredge-more:hover .fe-mark,
.foredge-more:hover .fe-seg { background: var(--bb-gilt-deep); }
.foredge-tab:focus-visible .fe-mark,
.foredge-more:focus-visible .fe-seg {
  box-shadow: 0 0 0 2px var(--bb-parchment), 0 0 0 3px var(--bb-gilt-deep);
}
/* THE FOOT — the founding, set text with no tab box, no numeral and no rule.
   Its `::before` is the rail's last segment, ending at the cross-stroke
   (`::after`), which is perpendicular to every lozenge and sits after the
   rail's end rather than on it: a terminus, not a mark. In the strip the text
   stands past the stroke and clears the end-fade by its own padding — a fade
   over the founding would say "there is more" at the one place there is not. */
.fe-founded {
  position: relative;
  flex: 0 0 auto;
  align-self: stretch;
  margin: 0;
  padding: 0.3rem 26px 0 14px;
  white-space: nowrap;
  font-family: var(--bb-font-italic);
  font-style: italic;
  font-size: 0.7rem;
  line-height: 1.25;
  color: var(--bb-ink-faint);
}
.fe-founded::before { right: auto; width: 8px; }
.fe-founded::after {
  content: "";
  position: absolute;
  left: 8px;
  top: -14px;
  width: 1px;
  height: 12px;
  background: var(--bb-gilt-deep);
  pointer-events: none;
}
.fe-founded i {
  display: block;
  font-style: normal;
  font-family: var(--bb-font-display);
  font-size: 0.62rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
}

/* THE SEALED WEEK'S PAGE HEAD — the live page's anatomy with the acts removed.
   Same versal, same date slot, so paging between weeks moves the eye no
   further than the numeral changes. */
.turn-page-head.is-sealed .turn-progress { font-family: var(--bb-font-italic); font-style: italic; color: var(--bb-ink-faint); }
/* The versal goes to ink on a sealed week. Vermillion is reserved for the week
   that is HAPPENING — it is why the numeral is set at display size in the first
   place — and a past week wearing it would say "now" on every page at once. */
.turn-page-head.is-sealed .turn-versal { color: var(--bb-ink-soft); }
/* THE COMPANION (#311): the milestone(s) dated to this week, named once, on
   the record surface. A short gilt rule rather than a box — the fore-edge's
   mark is gilt, and this is the line it points to. */
.turn-marked {
  position: relative;
  max-width: 46ch;
  margin: 0 0 0.9rem;
  padding: 7px 0 0;
  font-family: var(--bb-font-italic);
  font-style: italic;
  font-size: 0.9rem;
  color: var(--bb-ink-soft);
}
.turn-marked::before {
  content: "";
  position: absolute;
  top: 0;
  left: 0;
  width: 54px;
  border-top: 2px solid var(--bb-gilt);
}
.turn-marked b { font-weight: 400; font-style: normal; color: var(--bb-ink); }
/* The gallery's in-game date, in the milestone record's italic manuscript
   voice — the typographic split that says in-character from system (#311). */
.cpv-date { font-family: var(--bb-font-italic); font-style: italic; color: var(--bb-ink-soft); }
.turn-return {
  background: none;
  border: none;
  box-shadow: none;
  border-radius: 0;
  padding: 0.2rem 0;
  cursor: pointer;
  font-family: var(--bb-font-display);
  font-size: 0.9rem;
  color: var(--bb-gilt-deep);
  white-space: nowrap;
}
.turn-return:hover { color: var(--bb-gilt); }


/* ---- The reckoning (#296) ----------------------------------------------- */
/* THE RECKONING (#296) — a sealed week's account on the facing page. Rows whose
   figure is zero are never drawn (see renderReckoning), so what is on the page
   is what happened; a column of zeroes is five rows saying one thing. */
.reckon-sheet { display: flex; flex-direction: column; }
/* The net is the row the others exist to explain, so it is ruled off rather
   than merely last, and it drops the dotted under-rule that separates working
   rows from each other — nothing follows it in its own group. */
.reckon-row.is-net {
  border-bottom: none;
  border-top: 1px solid var(--bb-rule);
  margin-top: 0.25rem;
  padding-top: 0.45rem;
}
.reckon-row.is-net .stroll-label { color: var(--bb-ink); }
.reckon-row.is-goods .stroll-label { font-family: var(--bb-font-italic); font-style: italic; }
.reckon-quiet { margin: 0.2rem 0 0; font-family: var(--bb-font-italic); font-style: italic; color: var(--bb-ink-faint); }
.reckon-event { margin: 0.7rem 0 0; color: var(--bb-ink-soft); font-size: 0.9375rem; }
.reckon-event::before { content: "❧ "; color: var(--bb-gilt-deep); }
/* Why the facing page is not the one you left, with the way back attached. The
   fact register (`.chronicle-viewer-note`'s voice) — not an apology, not an
   alert: the reader asked for a past week and this is what a past week has. */
.reckon-aside {
  margin: 1.1rem 0 0;
  padding-top: 0.7rem;
  border-top: 1px dotted var(--bb-rule-soft);
  font-family: var(--bb-font-italic);
  font-style: italic;
  font-size: 0.8rem;
  color: var(--bb-ink-faint);
}

/* "Recorded this turn" (#192) — the live week's correction ledger on the open
   page: sealed-log row grammar plus a revert-and-delete × per row. */
.turn-recorded-log { margin-bottom: 0.9rem; }
.turn-recorded-log .turn-log-divider { margin-top: 1.1rem; }
.turn-recorded-row { display: flex; align-items: baseline; gap: 0.4rem; }
.turn-recorded-text { flex: 1 1 auto; min-width: 0; }


/* ---- Inline numeric + note editors -------------------------------------- */
/* #218 — Chronicle field vocabulary (single source of truth for the chapter's
   editable fields). The Chronicle is the bastion's ledger; its inputs had
   drifted to heavy full-bordered boxes that read as pasted-on form chrome
   against the parchment — out of step with the Household and Register. Re-cast
   them in the app's two established editable-field idioms so the chapter reads
   as one hand. Each field below keeps only its own width / font / padding /
   alignment; the frame lives here.

   Archetype A — the written line: the Household's recessed field (a parchment
   ground under a transparent frame with a 2px left rule that warms to gilt on
   touch). Prose and free-text ledger entries. */
.stores-note,
.stores-ledger-label,
.stores-ledger-value,
.turn-result-item,
.turn-arrival-name,
.turn-recruit-name,
.turn-gold-item,
.turn-event-label,
.week-lead,
.week-field {
  background: var(--bb-parchment);
  border: 1px solid transparent;
  border-left: 2px solid var(--bb-rule-soft);
  border-radius: 0;
  color: var(--bb-ink);
  transition: background-color 90ms ease, border-color 90ms ease;
}
.stores-note:hover,
.stores-ledger-label:hover,
.stores-ledger-value:hover,
.turn-result-item:hover,
.turn-arrival-name:hover,
.turn-recruit-name:hover,
.turn-gold-item:hover,
.turn-event-label:hover,
.week-lead:hover,
.week-field:hover { border-left-color: var(--bb-gilt); }
.stores-note:focus,
.stores-ledger-label:focus,
.stores-ledger-value:focus,
.turn-result-item:focus,
.turn-arrival-name:focus,
.turn-recruit-name:focus,
.turn-gold-item:focus,
.turn-event-label:focus,
.week-field:focus {
  outline: none;
  border-color: var(--bb-rule);
  border-left-color: var(--bb-gilt-deep);
  background: var(--bb-parchment-light);
}

/* Archetype B — the ruled slot: a fill-in-the-blank ledger line (transparent
   ground, one bottom rule) for figures written into the record. Lighter than a
   box, and the underline rhymes with the dotted leaders in the Stores rows and
   the existing date slots. Numerals stay ink — vermillion is the app's
   identity/alarm register, never "here's a value". */
.stores-num,
.turn-result-num,
.turn-duration,
.turn-event-roll {
  background: transparent;
  border: none;
  border-bottom: 1px solid var(--bb-rule-soft);
  border-radius: 0;
  /* Clear the global inset "sunken" shadow — a ruled slot must read as a flat
     line, not a recessed box (this is what still boxed the numbers). */
  box-shadow: none;
  color: var(--bb-ink);
  transition: background-color 90ms ease, border-color 90ms ease, box-shadow 90ms ease;
}
.stores-num:hover,
.turn-result-num:hover,
.turn-duration:hover,
.turn-event-roll:hover { border-bottom-color: var(--bb-gilt); }
/* Focus: a 2px gilt underline drawn as an inset shadow (no height shift, no
   re-boxing vermillion ring) — the date-slot idiom, in the gilt register. */
.stores-num:focus,
.turn-result-num:focus,
.turn-duration:focus,
.turn-event-roll:focus {
  outline: none;
  border-bottom-color: var(--bb-gilt-deep);
  background: var(--bb-parchment);
  box-shadow: inset 0 -2px 0 var(--bb-gilt-deep);
}
.turn-event-roll[aria-invalid="true"],
.turn-casualty-ones[aria-invalid="true"] { border-bottom-color: var(--bb-vermillion); }

/* Milestone in-game date (#87 rider): a faint dotted micro-affordance that
   becomes an inline input — the ck-name inline-edit look at meta scale. */
.milestone-date-slot { position: relative; display: inline-flex; align-items: baseline; }
.milestone-date-btn { background: none; border: none; box-shadow: none; padding: 0; cursor: pointer; font-family: var(--bb-font-italic); font-style: italic; font-size: 0.8125rem; color: var(--bb-ink-faint); text-decoration: underline dotted; text-underline-offset: 2px; }
.milestone-date-btn:hover { color: var(--bb-ink); }
.milestone-date-btn.has-date { color: var(--bb-ink-soft); text-decoration: none; }
/* A DERIVED milestone date (#310) reads as the app's reckoning rather than the
   reader's own words: it keeps the dotted underline (it is still an invitation
   to write over it) but takes the settled ink of a real date rather than the
   faint ink of an empty "+ date" prompt. Deliberately NOT `has-date`, which
   means "the reader typed this". */
.milestone-date-btn.is-derived { color: var(--bb-ink-soft); }
.milestone-date-btn.has-date:hover { text-decoration: underline dotted; text-underline-offset: 2px; }
.milestone-date-input { width: 9rem; min-width: 0; background: transparent; border: none; border-bottom: 1px dotted var(--bb-rule); border-radius: 0; box-shadow: none; font-family: var(--bb-font-italic); font-style: italic; font-size: 0.8125rem; color: var(--bb-ink); padding: 0; }
.milestone-date-input:focus { outline: none; border-bottom-color: var(--bb-ink-soft); }
/* THE WEEK FOOT (#311) — under the open date input, only when a calendar is
   set: the campaign's weeks by date, newest first, "not dated" last. A ruled
   list of lines and deliberately NOT the fore-edge's tab vocabulary: those
   tabs are jump buttons, and a picker wearing them would read as navigation.
   Full dates, no elision, no turn numbers — see buildMilestoneDateSlot. */
.md-foot {
  position: absolute;
  z-index: 40;
  left: 0;
  top: 100%;
  min-width: 214px;
  max-width: 240px;
  margin-top: 4px;
  padding: 7px 0 4px;
  background: var(--bb-parchment-light);
  border: 1px solid var(--bb-rule-soft);
  box-shadow: 0 6px 18px -6px var(--bb-shadow-mid);
  font-style: normal;
}
.md-foot-head {
  display: block;
  margin: 0 0 4px;
  padding: 0 11px 6px;
  border-bottom: 1px solid var(--bb-rule-soft);
  font-family: var(--bb-font-italic);
  font-style: italic;
  font-size: 0.72rem;
  color: var(--bb-ink-faint);
}
.md-list { max-height: 168px; overflow-y: auto; scrollbar-gutter: stable; padding-right: 4px; }
.md-week {
  display: flex;
  width: 100%;
  align-items: baseline;
  gap: 0.4rem;
  padding: 3px 11px;
  background: none;
  border: none;
  border-radius: 0;
  box-shadow: none;
  text-align: left;
  cursor: pointer;
  color: inherit;
  font-family: var(--bb-font-text);
  font-size: 0.82rem;
  line-height: 1.35;
}
.md-week:hover { background: var(--bb-parchment-mid); }
.md-week:focus-visible { outline: 2px solid var(--bb-gilt-deep); outline-offset: -2px; }
.md-week em { margin-left: auto; font-style: italic; font-size: 0.74rem; color: var(--bb-ink-faint); }
.md-week.is-live em { color: var(--bb-vermillion); }
.md-week.is-on { background: var(--bb-gilt-fill); }
.md-clear {
  display: block;
  width: 100%;
  margin-top: 4px;
  padding: 6px 11px 4px;
  border: none;
  border-top: 1px solid var(--bb-rule-soft);
  border-radius: 0;
  background: none;
  box-shadow: none;
  text-align: left;
  cursor: pointer;
  font-family: var(--bb-font-italic);
  font-style: italic;
  font-size: 0.76rem;
  color: var(--bb-ink-faint);
}
.md-clear:hover { background: var(--bb-parchment-mid); color: var(--bb-ink); }

/* Charter vitals tile action (#112): a quiet in-tile text link. */
/* .fig-turn-link and .fig-note were the figures grid's action and annotation
   voices. #301 retired the grid; the colophon says both with .sv-verb and the
   Week figure's own unit, so neither class has a user. */

/* Register order-card echo (#112): a small this-turn line under the outcome
   prose; doubles as the pre-start discoverability hint (ink-faint). */
.reg-turn-echo { margin: 0.4rem 0 0; padding-top: 0.35rem; border-top: 1px solid var(--bb-rule-soft); font-size: 0.85rem; color: var(--bb-ink-soft); }
.reg-turn-hint { font-family: var(--bb-font-italic); font-style: italic; color: var(--bb-ink-faint); }

/* Household › In Residence: recruit cards share the #154 gallery idiom —
   grid + card rules live with the .house-card family above. */
#residence-block { margin-top: 1.5rem; }


/* ---- Help dialog + the plan band (#291) --------------------------------- */
/* Help dialog (§A, issue #71) and welcome dialog (§B) */
.modal-wide { max-width: 760px; }

/* Help dialog fills a bounded height and lets each pane scroll independently, so
   the content pane reaches the bottom instead of stopping at a fixed cap while
   the (grouped, now longer) nav column runs taller. Mirrors #settings-dialog. */
#help-dialog .dialog-form {
  max-height: 88vh;
}
#help-dialog .dialog-actions {
  flex-shrink: 0;
}
/* The dialog carries no nav rail at any width. It is opened by `?` (I'm stuck)
 * or an `i` glyph (what is this?) — both mid-task, both wanting one section, so
 * a 152px rail spent 20% of a 760px modal on a browse affordance the reader
 * wasn't using, and put a second scrollbar beside the first. Browsing is served
 * instead by .help-contents, prepended to the reading pane at full width. The
 * rail markup stays: it is still generated, and it is what the card is built
 * from. (This was the ≤640px phone rule; the phone tier had already proved the
 * layout, it was just reserved for phones.) */
#help-dialog .help-layout { grid-template-columns: 1fr; }
#help-dialog .help-nav { display: none; }

/* Plan band (#291) — a drawn miniature of the app's own chapter band, and the
 * first thing in the reading pane in both hosts. The contents card answers
 * "what is in this guide"; a newcomer who pressed `?` is asking "where am I",
 * and a 30-link documentation index is not that answer.
 *
 * It is not stacked ON the card — it is the card's first tier, which is why it
 * carries no bottom rule of its own (the card's closes the whole block) and why
 * the card's three orphan front-matter links move onto the caption row here.
 * That row was empty right-hand space, so they ride free, and column 1 of the
 * card gives back most of the height the band costs.
 *
 * Knowingly a picture of app chrome, which the app does nowhere else: if the
 * chapter band ever changes, this and chapters.js change with it. */
.help-plan { margin: 0; }

/* Pinned in the DIALOG only. The dialog hides the nav rail, so once a reader
 * jumps into a section there is nothing left to navigate with but the scrollbar
 * — the band is the only standing affordance the surface has. The standalone
 * page is deliberately excluded: `.help-page .help-nav` is already sticky, and
 * a second persistent nav beside it is two answers to one question.
 *
 * Opaque, because prose scrolls under the 3px gaps between leaves. The modal's
 * own ground is a light→mid gradient, so this reads as a slightly lighter plate
 * at the top of the pane rather than an invisible join — which is the honest
 * result: it IS a plate, and it is now the one part of the pane that does not
 * scroll. */
#help-dialog .help-plan {
  position: sticky;
  top: 0;
  z-index: 2;
  padding-top: 2px;
  background: var(--bb-parchment-light);
}
/* Forced by the sticky band, not decoration: scrollIntoView({block:"start"})
 * lands a section's heading at the top of the scrollport, which is UNDER the
 * pinned band — every jump-link would arrive on a hidden heading. Mirrors the
 * standalone page's own `.help-page .help-section` offset under its sticky
 * rail. Measured against the rendered band, plus a few px of air. */
#help-dialog .help-section { scroll-margin-top: 64px; }

/* The leaves bridge into one ink baseline rule — the ledger-tab affordance of
 * .chapter-nav, at reading scale. Deliberately no -1px overlap and no hover
 * lift: those sell "this is a control you press", and these are jump-links. */
.help-plan-band {
  display: flex;
  align-items: stretch;
  gap: 3px;
  border-bottom: 1px solid var(--bb-rule);
}
/* Content-width leaves, grown to fill — the real band's tabs are content-sized
 * too, and equal fifths force "Register of Chambers" to wrap at every width the
 * dialog has. The column + margin-top:auto keeps every gloss on one baseline
 * when a narrow pane does make a name wrap. */
.help-content .help-plan-leaf {
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  flex-direction: column;
  padding: 6px 9px 7px;
  background: var(--bb-parchment-mid);
  border: 1px solid var(--bb-rule-soft);
  border-bottom: none;
  color: var(--bb-ink);
  text-decoration: none;
  transition: background-color 90ms ease, color 90ms ease;
}
/* Same oldstyle-italic gilt as .chapter-tab[aria-selected] .numeral. Below AA,
 * which is acceptable only because it is aria-hidden decoration duplicating the
 * row's own order — exactly as the real band's numerals already are. */
.help-plan-numeral {
  margin-right: 5px;
  font-family: var(--bb-font-italic);
  font-style: italic;
  font-size: 0.72rem;
  color: var(--bb-gilt-deep);
}
.help-plan-name {
  font-family: var(--bb-font-display);
  font-size: 0.78rem;
  letter-spacing: 0.05em;
}
.help-plan-gloss {
  display: block;
  margin-top: auto;
  padding-top: 2px;
  color: var(--bb-ink-soft);
  font-size: 0.6875rem;
  line-height: 1.25;
}
.help-content .help-plan-leaf:hover {
  background: var(--bb-parchment-light);
}
.help-content .help-plan-leaf:hover .help-plan-name { color: var(--bb-vermillion); }
/* Explicit: a new box inherits nothing useful here, and the contents card's
 * link rule below zeroes the outline. */
.help-content .help-plan-leaf:focus-visible {
  outline: 2px solid var(--bb-vermillion);
  outline-offset: 2px;
}

/* The band's sibling, not its child — it must scroll away while the band pins. */
.help-plan-caption {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: 6px 16px;
  margin: 6px 0 14px;
}
.help-content .help-plan-caption p {
  margin: 0;
  color: var(--bb-ink-faint);
  font-family: var(--bb-font-italic);
  font-style: italic;
  font-size: 0.72rem;
}
.help-plan-front {
  display: flex;
  flex-wrap: wrap;
  gap: 4px 14px;
}
.help-content .help-plan-front a {
  color: var(--bb-ink-soft);
  font-family: var(--bb-font-display);
  font-size: 0.75rem;
  letter-spacing: 0.03em;
  text-decoration: none;
}
.help-content .help-plan-front a:hover,
.help-content .help-plan-front a:focus-visible {
  color: var(--bb-vermillion);
}

/* Phone tier (#283 query, byte-identical). The tab metaphor doesn't survive
 * stacking, so the band becomes five ruled rows at a 44px tap minimum, each
 * reading numeral · name — gloss on one line. The band drops its baseline rule:
 * the last row's own rule closes the plate, and both would double. */
@media (max-width: 700px), (max-height: 500px) and (pointer: coarse) {
  /* Never pinned here: five 44px rows is ~220px, which is most of a phone's
   * dialog. ID-prefixed to beat the ID-prefixed rule above — a bare .help-plan
   * would lose on specificity and the band would stay stuck. */
  #help-dialog .help-plan {
    position: static;
    padding-top: 0;
    background: none;
  }
  #help-dialog .help-section { scroll-margin-top: 0; }
  .help-plan-band {
    display: block;
    border-bottom: none;
  }
  .help-content .help-plan-leaf {
    flex-direction: row;
    align-items: baseline;
    gap: 5px;
    min-height: 44px;
    padding: 12px 2px;
    background: none;
    border: none;
    border-bottom: 1px solid var(--bb-rule-soft);
  }
  .help-plan-gloss {
    margin-top: 0;
    padding-top: 0;
  }
  .help-plan-gloss::before { content: "— "; }
  .help-plan-caption { display: block; }
  .help-plan-front { margin-top: 6px; }
}

/* Contents card — the codex's table of contents. Column-flows so 30-odd links
 * read across the pane instead of down it, with the nav's own group headers
 * kept as the section breaks. */
/* Three columns, not two: the whole index has to clear the fold, or the reader
 * who pressed `?` meets a screenful of contents instead of the guide. */
.help-contents {
  margin: 0 0 16px;
  padding-bottom: 12px;
  border-bottom: 1px solid var(--bb-rule-soft);
  columns: 3;
  column-gap: 20px;
}
.help-contents-lead,
.help-contents-group {
  break-inside: avoid;
  margin-bottom: 10px;
}
.help-contents h4 {
  margin: 0 0 3px;
  padding-bottom: 3px;
  border-bottom: 1px solid var(--bb-rule-soft);
  color: var(--bb-gilt-deep);
  font-family: var(--bb-font-display);
  font-size: 0.6875rem;
  font-weight: 600;
  letter-spacing: 0.12em;
  text-transform: uppercase;
}
/* Scoped through .help-content: the card sits inside the reading pane, and the
 * bare `.help-content a` prose rule below is same-specificity but later, so it
 * would otherwise paint every contents link vermillion. */
.help-content .help-contents a {
  display: block;
  padding: 2px 0;
  color: var(--bb-ink-soft);
  font-family: var(--bb-font-display);
  font-size: 0.75rem;
  letter-spacing: 0.03em;
  text-decoration: none;
  line-height: 1.25;
}
.help-content .help-contents a:hover,
.help-content .help-contents a:focus-visible {
  color: var(--bb-vermillion);
  outline: none;
}
@media (max-width: 640px) {
  .help-contents { columns: 1; }
}
@media (min-width: 641px) and (max-width: 820px) {
  .help-contents { columns: 2; }
}
/* In-dialog Help search (#173). A full-width bar pinned between the title and
   the two scrolling columns — belongs to neither, so it stays put while nav and
   content scroll under it. Filters the rendered sections client-side. */
.help-search {
  position: relative;
  flex: none;
  margin: 0 0 14px;
}
.help-search-input {
  width: 100%;
  padding-right: 2rem; /* room for the ✕ */
}
.help-search-input::placeholder {
  color: var(--bb-ink-faint);
  font-family: var(--bb-font-italic);
  font-style: italic;
}
/* Themed clear ✕ — faint ink at rest, vermillion on hover: the app's standard
   language for a control that erases what you typed rather than acting on it. */
.help-search-clear {
  position: absolute;
  right: 8px;
  top: 50%;
  transform: translateY(-50%);
  background: none;
  border: none;
  padding: 0 2px;
  color: var(--bb-ink-faint);
  cursor: pointer;
  font-size: 1rem;
  line-height: 1;
}
.help-search-clear:hover { color: var(--bb-vermillion); }
/* Keep centering under the global button:active nudge (see .house-suggest). */
.help-search-clear:active { transform: translateY(calc(-50% + 1px)); }
.help-search-clear:focus-visible { outline: 2px solid var(--bb-vermillion); outline-offset: 1px; }
/* Matched-term highlight — gilt (neutral/temporal), never vermillion (alert)
   or sapphire (reserved). Text stays ink for AA contrast. */
mark.help-hit {
  background: color-mix(in srgb, var(--bb-gilt) 28%, transparent);
  color: inherit;
  border-radius: 0;
  padding: 0 1px;
}
.help-empty {
  padding: 8px 0;
  font-family: var(--bb-font-italic);
  font-style: italic;
  color: var(--bb-ink-soft);
}
.help-empty span {
  display: block;
  margin-top: 4px;
  color: var(--bb-ink-faint);
}
.help-layout {
  display: grid;
  grid-template-columns: 152px 1fr;
  gap: 0 20px;
  flex: 1 1 auto;
  min-height: 0;
}
.help-nav {
  display: flex;
  flex-direction: column;
  gap: 1px;
  border-right: 1px solid var(--bb-rule-soft);
  padding-right: 16px;
  padding-top: 2px;
  overflow-y: auto;
  min-height: 0;
}
.help-nav a {
  display: block;
  padding: 5px 0;
  color: var(--bb-ink-soft);
  font-family: var(--bb-font-display);
  font-size: 0.8125rem;
  letter-spacing: 0.06em;
  text-decoration: none;
  text-transform: uppercase;
  line-height: 1.3;
}
.help-nav a:hover,
.help-nav a:focus-visible {
  color: var(--bb-vermillion);
  outline: none;
}
/* Category header in the nav rail (#117 follow-up): a quiet gilt label with a
   hairline rule, grouping the section links beneath it. Not a link. */
.help-nav-group {
  margin-top: 14px;
  padding-bottom: 3px;
  border-bottom: 1px solid var(--bb-rule-soft);
  color: var(--bb-gilt-deep);
  font-family: var(--bb-font-display);
  font-size: 0.6875rem;
  font-weight: 600;
  letter-spacing: 0.12em;
  text-transform: uppercase;
}
.help-nav-group:first-child {
  margin-top: 0;
}
/* Back matter — the guide's second and last tier. A codex has front matter, a
 * body, and an appendix, so the divider is the whole signal: a heavier rule
 * above it than the hairlines between ordinary groups, and its links a step
 * quieter. Deliberately ONE differentiator and no new component: importance is
 * never encoded in body contrast (that reads as disabled and fights AA), and
 * per-section badges would spend the gilt/vermillion status vocabulary on
 * documentation taxonomy. The middle tier — "optional feature" — is carried by
 * a sentence in the section's own first line, not by chrome. */
.help-nav-group.nav-group-reference,
.help-contents-group.nav-group-reference > h4 {
  margin-top: 20px;
  padding-top: 12px;
  border-top: 2px solid var(--bb-rule);
}
.help-nav .nav-group-reference ~ a,
.help-contents-group.nav-group-reference a { color: var(--bb-ink-faint); }
.help-nav .nav-group-reference ~ a:hover,
.help-nav .nav-group-reference ~ a:focus-visible,
.help-content .help-contents-group.nav-group-reference a:hover,
.help-content .help-contents-group.nav-group-reference a:focus-visible {
  color: var(--bb-vermillion);
}
.help-content {
  overflow-y: auto;
  min-height: 0;
  padding-right: 4px;
}
.help-content h3 {
  font-family: var(--bb-font-display);
  font-size: 0.9375rem;
  color: var(--bb-vermillion-deep);
  margin: 0 0 10px;
  padding-bottom: 4px;
  border-bottom: 1px solid var(--bb-rule-soft);
  letter-spacing: 0.04em;
}

/* Collapsible help sections (#94). Each section is a <details>; its <summary>
   carries the section heading and a chevron. Getting started ships open. */
.help-section {
  border-bottom: 1px solid var(--bb-rule-soft);
}
.help-section:last-child { border-bottom: none; }
.help-section[open] { padding-bottom: 8px; }
.help-section > summary {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 12px 0;
  cursor: pointer;
  list-style: none;
}
.help-section > summary::-webkit-details-marker { display: none; }
.help-section > summary h3 {
  margin: 0;
  padding-bottom: 0;
  border-bottom: none;
}
.help-section > summary::after {
  content: "";
  flex: none;
  width: 7px;
  height: 7px;
  margin-right: 3px;
  border-right: 1.5px solid var(--bb-ink-soft);
  border-bottom: 1.5px solid var(--bb-ink-soft);
  transform: rotate(45deg);
  transition: transform 0.15s ease, border-color 0.15s ease;
}
.help-section[open] > summary::after { transform: rotate(-135deg); }
.help-section > summary:hover h3,
.help-section > summary:focus-visible h3 { color: var(--bb-vermillion); }
.help-section > summary:hover::after,
.help-section > summary:focus-visible::after { border-color: var(--bb-vermillion); }
.help-section > summary:focus-visible {
  outline: 2px solid var(--bb-vermillion);
  outline-offset: 2px;
}
.help-content p {
  margin: 0 0 10px;
  font-family: var(--bb-font-text);
  font-size: 0.9375rem;
  line-height: 1.5;
  color: var(--bb-ink-soft);
}
.help-content p:last-child { margin-bottom: 0; }
/* …but the reset above is for the END OF THE READING PANE, and `p:last-child`
   reaches deeper than that. `.help-content h3` carries no top margin — a
   heading's leading gap IS the previous paragraph's 10px — so any wrapper
   ending in a paragraph welds the next heading onto it. #266's two marker divs
   are exactly that: `#relay-absent` / `#relay-sections` are serve.py's swap
   anchors on privacy.html, structural rather than visual, and both end in a
   paragraph immediately before "Who operates this instance". Whichever one a
   copy serves, the gap vanished. Anchored on the ids for the same reason
   serve.py is — the prose inside them is rewritten often; the anchors are not. */
#relay-absent > p:last-child,
#relay-sections > p:last-child { margin-bottom: 10px; }
.help-content a { color: var(--bb-vermillion); text-underline-offset: 2px; }
.help-content strong { color: var(--bb-ink); }
.help-content h4 {
  font-family: var(--bb-font-display);
  font-size: 0.875rem;
  color: var(--bb-ink);
  margin: 16px 0 6px;
  font-weight: 600;
  letter-spacing: 0.02em;
}
.help-content ul {
  margin: 0 0 10px;
  padding-left: 1.25em;
  font-family: var(--bb-font-text);
  font-size: 0.9375rem;
  color: var(--bb-ink-soft);
}
.help-content li {
  margin-bottom: 4px;
  line-height: 1.5;
}
/* Numbered "Getting started" steps — gilt counters in the display face so the
   onboarding track reads like a handout's lettered instructions (#94). */
.help-content ol.help-steps {
  margin: 0 0 10px;
  padding-left: 0;
  list-style: none;
  counter-reset: help-step;
  font-family: var(--bb-font-text);
  font-size: 0.9375rem;
  color: var(--bb-ink-soft);
}
.help-content ol.help-steps > li {
  counter-increment: help-step;
  position: relative;
  padding-left: 2em;
  margin-bottom: 8px;
  line-height: 1.5;
}
/* A step's leading bold run is its title, so it takes its own line. Without
 * this the step reads as numbered prose — one sentence carrying four facts —
 * which is the thing a to-do list is supposed to not be. Editorial half of the
 * rule: the lead is a verb phrase of five words or fewer. */
.help-content ol.help-steps > li > strong:first-child {
  display: block;
  color: var(--bb-ink);
  margin-bottom: 1px;
}
.help-content ol.help-steps > li::before {
  content: counter(help-step);
  position: absolute;
  left: 0;
  top: 0;
  width: 1.4em;
  text-align: center;
  font-family: var(--bb-font-display);
  font-size: 0.875rem;
  color: var(--bb-gilt-deep);
  font-weight: 600;
}
.keys-list {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: 6px 16px;
  margin: 0 0 10px;
  padding: 0;
  font-size: 0.875rem;
  color: var(--bb-ink-soft);
}
.keys-list dt { margin: 0; align-self: center; }
.keys-list dd { margin: 0; line-height: 1.4; align-self: center; }
/* Reference tables whose terms are words, not keystrokes (Settings' option
 * groups, a note's controls). The shortcuts list sizes its term column to the
 * widest <kbd>; a prose term needs a bounded column and top alignment, since
 * its definition runs to several lines. */
.help-defs { grid-template-columns: minmax(0, 11em) 1fr; gap: 10px 16px; }
.help-defs dt { align-self: start; color: var(--bb-ink); }
.help-defs dd { align-self: start; }
@media (max-width: 640px) {
  .help-defs { grid-template-columns: 1fr; gap: 2px 0; }
  .help-defs dd { margin-bottom: 8px; }
}
.keys-list kbd {
  display: inline-block;
  font-family: var(--bb-font-mono);
  font-size: 0.8125rem;
  font-variant-numeric: lining-nums tabular-nums;
  background: var(--bb-parchment-mid);
  border: 1px solid var(--bb-rule);
  padding: 1px 6px;
  white-space: nowrap;
}
.help-about-version {
  font-family: var(--bb-font-mono);
  font-size: 0.84rem;
  color: var(--bb-ink-soft);
  margin-top: 6px;
}

/* ---- Standalone Help page (#174) ---------------------------------------- */
/* The in-app guide, published as its own page (web/help.html) so it can sit
 * beside the app in a second tab. Reuses every .help-* style above; the rules
 * below are only the deltas from the modal context — a bounded centered column,
 * one page scroll (not two inner scrollbars), and a sticky nav rail. Scoped to
 * .help-page / .help-page-bar so the in-app dialog is untouched. */

/* Tier-1 chrome: the app top-bar recipe, pinned so the brand + search stay
 * reachable while the guide scrolls. Opaque (the app bar is translucent, which
 * would let scrolled content bleed through on a scrolling page). */
.help-page-bar {
  position: sticky;
  top: 0;
  z-index: 20;
  background: linear-gradient(180deg, var(--bb-parchment-light), var(--bb-parchment-mid));
}
.help-page-bar a { text-decoration: none; }
/* Search sits at the right of the bar; shrinks before the brand does. */
.help-page-bar .help-search {
  flex: 0 1 280px;
  min-width: 0;
  margin: 0;
}

.help-page {
  max-width: 880px;
  margin-inline: auto;
  padding: 18px 24px 40px;
}
.help-page .dialog-title { margin-top: 0; }

/* The dialog bounds each pane and scrolls it internally; on a full page we want
 * a single document scroll instead. */
.help-page .help-nav,
.help-page .help-content {
  overflow: visible;
}

/* Nav rail follows the reader; clears the sticky top-bar (~52px) and scrolls on
 * its own only if it outgrows the viewport. */
.help-page .help-nav {
  position: sticky;
  top: 58px;
  align-self: start;
  max-height: calc(100vh - 68px);
  overflow-y: auto;
}
/* Jump-links and native Ctrl+F must not land under the sticky bar. */
.help-page .help-section { scroll-margin-top: 66px; }

/* Phone: drop the nav rail (all sections default open + search cover it) and go
 * single-column. The back-link stays — it's the only way back without switching
 * tabs, unlike the app's in-chrome Help button. */
@media (max-width: 640px) {
  .help-page { padding: 14px 16px 32px; }
  .help-page .help-layout { grid-template-columns: 1fr; }
  .help-page .help-nav { display: none; }
}

/* ---- Themed tooltip (§G) ------------------------------------------------ */

.bb-tooltip {
  position: fixed;
  z-index: 9000;
  display: none;
  max-width: 300px;
  padding: 6px 10px;
  background: var(--bb-parchment-light);
  border: 1px solid var(--bb-rule);
  border-radius: 0;
  font-family: var(--bb-font-text);
  font-size: 0.84rem;          /* 13.5px */
  line-height: 1.4;
  color: var(--bb-ink-soft);
  box-shadow: 0 10px 24px -8px var(--bb-shadow-mid);
  pointer-events: none;
}
.bb-tooltip.visible { display: block; }
.bb-tip-title {
  font-family: var(--bb-font-display);
  font-size: 0.84rem;          /* 13.5px */
  color: var(--bb-ink);
  line-height: 1.2;
  padding-bottom: 5px;
  margin-bottom: 5px;
  border-bottom: 1px solid var(--bb-rule-soft);
}
.bb-tip-key {
  display: inline-block;
  margin-left: 6px;
  font-family: var(--bb-font-mono);
  font-size: 0.75rem;
  font-variant-numeric: lining-nums tabular-nums;
  line-height: 1.3;
  color: var(--bb-ink);
  background: var(--bb-parchment-mid);
  border: 1px solid var(--bb-rule);
  border-radius: 0;
  padding: 0 5px;
  white-space: nowrap;
  vertical-align: baseline;
}

/* Placement-banner pb-icon kind override (§2.5): special = vermillion
 * (default), basic = sapphire. Both render the glyph in parchment-light. */
.placement-banner .pb-icon.basic {
  background: var(--bb-sapphire);
  color: var(--bb-parchment-light);
}

/* Picker panels inside the rail need to not bust the rail width. */

/* Status toast — when the page is in single-column mode, give it more room. */
@media (max-width: 1000px) {
  .status-toast { bottom: 12px; }
}

/* The `hidden` attribute must always win over our display: flex/grid rules. */
[hidden] { display: none !important; }

/* Disabled picker item — used when the Special-facility option is at cap. */
.picker-list li.disabled,
.picker-list li[aria-disabled="true"] {
  opacity: 0.5;
  cursor: not-allowed;
}
.picker-list li.disabled:hover,
.picker-list li[aria-disabled="true"]:hover {
  background: transparent;
  border-color: var(--bb-rule-soft);
}

/* Idle placement-banner state (§6.1): calm parchment-mid hint strip. No
 * pulse, dashed pb-icon, no chip background on the status text. */
/* Two-weight status zone (#230): idle recedes to a quiet caption so it stops
 * competing with the toolbar above it; arming a tool lights the zone back up
 * (the .mode / placing states are untouched). The row keeps its reserved
 * height either way — content swaps in place, never visibility, so the canvas
 * never reflows and the aria-live region is never torn down. */
.placement-banner.idle {
  background: transparent;
  border-color: var(--bb-rule-soft);
  box-shadow: none;
  animation: none;
  color: var(--bb-ink-faint);
}
.placement-banner.idle .pb-icon {
  /* Drop the disc entirely: a bare, muted inline mark — no fill, no ring — so
   * idle reads as a caption line, not a boxed banner. */
  background: transparent;
  border: none;
  color: var(--bb-ink-faint);
  font-weight: 400;
  font-size: 0.9rem;
}
.placement-banner.idle .pb-text strong {
  /* Muted caption weight — no display-face bolding, no confident ink. */
  color: var(--bb-ink-faint);
  font-family: var(--bb-font-display);
  font-weight: 500;
  font-style: normal;
  letter-spacing: 0.01em;
}
.placement-banner.idle .pb-text small {
  font-family: var(--bb-font-italic);
  font-style: italic;
  color: var(--bb-ink-faint);
}
/* Keep the Cancel button in layout when idle so the banner has the same
 * height whether idle or active — just hide it visually + from focus. */
.placement-banner.idle #btn-cancel-placement { visibility: hidden; }

/* Haunted badge — eerie corner mark on placed facility rects (SVG). */
.haunted-badge { pointer-events: none; }
.haunted-badge-bg {
  fill: var(--bb-parchment);
  stroke: var(--bb-haunted);
  stroke-width: 1;
  opacity: 0.92;
}

/* Under-construction badge (#195) — same corner-disc grammar, gilt instead
 * of haunted's violet; top-right so it never collides with the haunted mark. */
.construction-badge { pointer-events: none; }
.construction-badge-bg {
  fill: var(--bb-parchment);
  stroke: var(--bb-gilt);
  stroke-width: 1;
  opacity: 0.92;
}

/* =========================================================================
 * Facility hover tooltip (floor canvas)
 * ========================================================================= */
/* Facility hover tooltip (§6.5.11) — parchment-light gradient with a
 * per-kind outer halo ring (no colored border — the halo is the hue tag). */
#facility-tooltip {
  position: fixed;
  z-index: 1000;
  pointer-events: none;
  background: linear-gradient(180deg, var(--bb-parchment-light), var(--bb-parchment-mid));
  border: 1px solid var(--bb-rule);
  border-radius: 0;
  padding: 8px 12px 9px;
  box-shadow: 0 10px 24px -8px var(--bb-shadow-deep);
  opacity: 0;
  transform: translateY(4px);
  transition: opacity 100ms ease, transform 100ms ease;
  min-width: 160px;
  max-width: 260px;
}
#facility-tooltip.visible { opacity: 1; transform: translateY(0); }
#facility-tooltip.special {
  box-shadow:
    0 10px 24px -8px var(--bb-shadow-deep),
    0 0 0 3px var(--bb-special-ring);
}
#facility-tooltip.basic {
  box-shadow:
    0 10px 24px -8px var(--bb-shadow-deep),
    0 0 0 3px var(--bb-basic-ring);
}
#facility-tooltip .tt-name {
  font-family: var(--bb-font-display);
  font-size: 0.875rem;             /* 14px */
  line-height: 1.15;
  color: var(--bb-ink);
}
#facility-tooltip .tt-meta {
  margin-top: 4px;
  font-family: var(--bb-font-display);
  font-size: 0.6875rem;             /* 11px */
  letter-spacing: 0.20em;
  text-transform: uppercase;
  color: var(--bb-ink-faint);
}
#facility-tooltip .tt-meta .dot { color: var(--bb-rule-soft); margin: 0 6px; }
#facility-tooltip .tt-sub {
  color: var(--bb-vermillion);
  font-family: var(--bb-font-italic);
  font-style: italic;
}
/* Catalog one-liner (#117). Line-clamped — the tooltip floats over the
 * floorplan and must never blanket it. */
#facility-tooltip .tt-desc {
  margin-top: 5px;
  font-family: var(--bb-font-text);
  font-size: 0.8125rem;              /* 13px */
  line-height: 1.35;
  color: var(--bb-ink-soft);
  display: -webkit-box;
  -webkit-line-clamp: 3;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
#facility-tooltip .tt-orders {
  margin-top: 4px;
  font-family: var(--bb-font-text);
  font-size: 0.8125rem;              /* 13px */
  color: var(--bb-ink-soft);
}

/* =========================================================================
 * Credits footer — fixed bottom-right corner so it never reflows the
 * floorplan-height-100vh layout.
 * ========================================================================= */
/* Version badge (Codex §8) — channel-aware build pill. Now lives in the
 * Settings → About row (it used to float in a fixed bottom-right footer). */
.version-badge {
  display: inline-block;
  position: relative;
  padding: 1px 7px;
  border-radius: 0;                 /* squared per §2.4 */
  font-family: var(--bb-font-mono);
  font-size: 0.75rem;             /* 12px */
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  vertical-align: 1px;
  user-select: none;
  cursor: pointer;
  opacity: 1;
  transition: filter 0.15s;
  border: 1px solid var(--bb-rule);
}
.version-badge:hover { filter: brightness(1.06); }
.version-badge[data-channel="release"] {
  background: var(--bb-parchment-mid);
  color: var(--bb-ink-soft);
  border-color: var(--bb-rule);
}
.version-badge[data-channel="edge"] {
  background: rgba(168,120,38,0.18);
  color: var(--bb-gilt-deep);
  border-color: var(--bb-gilt);
}
/* Barber-pole striped pill — "you're running an unbuilt working tree". */
.version-badge[data-channel="dev"] {
  background: repeating-linear-gradient(
    135deg,
    var(--bb-parchment-mid),
    var(--bb-parchment-mid) 4px,
    var(--bb-parchment-deep) 4px,
    var(--bb-parchment-deep) 8px
  );
  color: var(--bb-ink-soft);
  border-color: var(--bb-rule);
}
.version-badge.has-update {
  outline: 2px solid var(--bb-gilt);
  outline-offset: 2px;
}
.version-badge.has-update::after {
  content: "";
  position: absolute;
  top: -4px;
  right: -4px;
  width: 8px;
  height: 8px;
  background: var(--bb-vermillion);
  border-radius: 0;
}
/* #164 — the same update cue mirrored onto persistent chrome: the top-bar
   Settings button, and the phone-tier "⋯ More" trigger it collapses into. Dot
   ONLY here, no gilt outline: on an interactive control the outline reads as a
   focus ring (the static badge keeps its outline because it never takes focus).
   The vermillion dot is a sanctioned exception to the #131 vermillion-is-alert
   rule, reserved for the update cue. */
#btn-settings.has-update,
#btn-more-menu.has-update {
  position: relative;
}
#btn-settings.has-update::after,
#btn-more-menu.has-update::after {
  content: "";
  position: absolute;
  top: -4px;
  right: -4px;
  width: 8px;
  height: 8px;
  background: var(--bb-vermillion);
  border-radius: 0;
}
/* Changelog "What's new" body: fetched CHANGELOG.md is rendered to real
   markup client-side (help.js highlightsToHtml), so it needs its own
   typography rather than the plain-text .dialog-body pre-line default. */
.changelog-body {
  white-space: normal;
}
.changelog-body h3 {
  font-family: var(--bb-font-display);
  font-size: 0.9375rem;
  color: var(--bb-vermillion-deep);
  margin: 18px 0 8px;
  padding-bottom: 4px;
  border-bottom: 1px solid var(--bb-rule-soft);
  letter-spacing: 0.04em;
}
.changelog-body h3:first-child { margin-top: 0; }
.changelog-body ul {
  margin: 0 0 4px;
  padding-left: 1.25em;
  font-family: var(--bb-font-text);
  font-size: 0.84rem;
}
.changelog-body li {
  margin-bottom: 8px;
  line-height: 1.55;
}
.changelog-body a { color: var(--bb-vermillion); text-underline-offset: 2px; }
.changelog-body strong { color: var(--bb-ink); }
.changelog-body kbd {
  display: inline-block;
  font-family: var(--bb-font-mono);
  font-size: 0.78rem;
  background: var(--bb-parchment-mid);
  border: 1px solid var(--bb-rule);
  padding: 0 5px;
  white-space: nowrap;
}

/* Level-up digest (#252) — the celebratory twin of the compliance notice, in the
   shared confirm dialog. `.levelup-body` restores normal white-space (the enclosing
   .dialog-body is pre-line). Gilt-keyed (earned-good), where compliance is vermillion. */
.levelup-body { white-space: normal; }
.levelup-head {
  display: flex;
  align-items: center;
  gap: 12px;
  padding-bottom: 12px;
  margin-bottom: 12px;
  border-bottom: 1px solid var(--bb-rule-soft);
}
.levelup-glyph { flex: none; width: 40px; height: 40px; color: var(--bb-gilt-deep); }
.levelup-glyph .glyph-svg { width: 100%; height: 100%; }
.levelup-hero { margin: 0; display: flex; flex-wrap: wrap; align-items: baseline; gap: 2px 8px; }
.levelup-hero-num {
  font-family: var(--bb-font-display);
  font-size: 1.5rem;
  color: var(--bb-gilt-deep);
  line-height: 1;
}
.levelup-hero-label { font-family: var(--bb-font-display); font-size: 0.9375rem; color: var(--bb-ink); }
.levelup-hero-total {
  flex-basis: 100%;
  font-family: var(--bb-font-italic);
  font-style: italic;
  font-size: 0.875rem;
  color: var(--bb-ink-soft);
}
.levelup-avail { margin: 0; color: var(--bb-ink-soft); }
.levelup-avail strong { color: var(--bb-ink); }
.levelup-subhead {
  font-family: var(--bb-font-display);
  font-size: 0.9375rem;
  color: var(--bb-gilt-deep);
  margin: 16px 0 8px;
  padding-bottom: 4px;
  border-bottom: 1px solid var(--bb-rule-soft);
  letter-spacing: 0.04em;
}
.levelup-improved-list { list-style: none; margin: 0; padding: 0; }
.levelup-improved-list li { display: flex; flex-direction: column; margin-bottom: 8px; }
.levelup-improved-name { font-family: var(--bb-font-text); color: var(--bb-ink); }
.levelup-improved-note {
  font-family: var(--bb-font-italic);
  font-style: italic;
  font-size: 0.875rem;
  color: var(--bb-ink-soft);
}

/* Vehicle hull outline on the floorplan */
.vehicle-hull-outline {
  fill: none;
  stroke: var(--bb-ink);
  stroke-width: 5;
  opacity: 0.45;
  stroke-linejoin: round;
  stroke-linecap: round;
}

/* Vehicle type badge in the sidebar identity section */
.bastion-type-badge {
  font-family: var(--bb-font-italic);
  font-size: 0.78125rem;
  font-style: italic;
  color: var(--bb-ink-soft);
  letter-spacing: 0.02em;
}

/* Disabled mode buttons (e.g. Defenses on vehicle bastions, Note when
 * marginalia is hidden) */
.mode-btn:disabled,
.mode-btn:disabled:hover {
  opacity: 0.38;
  background: var(--bb-parchment-light);
  color: var(--bb-ink-soft);
  border-color: var(--bb-rule);
  cursor: not-allowed;
}

/* =========================================================================
 * Tablet / narrow-window tier (#218) — the top bar folds before it breaks.
 *
 * The ⋯ fold used to begin at the phone tier, so the whole 701–1024px band had
 * no fold path at all: .top-bar-spacer collapsed to zero around 960px and the
 * bar overflowed .app-shell's grid column from there down (measured: 921px of
 * content in a 905px window, with Help clipped off the right edge). Tablet
 * portrait, iPad, and any half-width desktop window sat inside that band.
 *
 * Folded here: Export map ▾ / Settings / Help — the consult-and-configure
 * verbs. NOT folded: Open handout (the band's only gilt CTA and its right-hand
 * anchor; a tablet's band is not scarce enough to trade it for a lone ⋯) and
 * the autosave colophon (its phone-tier hide is a scarcity argument, and this
 * fold frees far more than the note costs).
 *
 * 1024px rather than the measured ~960: it clears the collapse with headroom
 * for display faces that cannot be fixed per-theme (Cartographer's Cinzel is
 * much wider, and per-theme rules may change values, never density), and it is
 * iPad landscape — so a tablet keeps one chrome through a rotation instead of
 * changing shape between 768 and 1024.
 * ========================================================================= */
@media (max-width: 1024px) {
  .more-menu-wrap { display: inline-block; }
  .export-map-wrap,
  #btn-settings,
  #btn-help { display: none; }
}

/* Open handout stays a button above the phone tier, so the panel must not
 * repeat the verb the bar already carries. Scoped as a range because ⋯ does not
 * exist above 1024px; the phone tier re-shows the row for free by hiding the
 * button instead.
 *
 * The negation carries that "for free" through the tier's height branch (#283).
 * A landscape phone clears min-width:701px, so without it the tier would hide
 * `#btn-export-handout` while this still hid the row — and Open handout would be
 * reachable from nowhere at all. */
@media (min-width: 701px) and (max-width: 1024px) and (not ((max-height: 500px) and (pointer: coarse))) {
  #more-menu [data-more="handout"] { display: none; }
}

/* =========================================================================
 * The Grounds' narrow tier (#275) — 701px to 1024px.
 *
 * The Grounds had a phone tier and a desktop tier and NOTHING between, so every
 * band from 701px up was unlaid-out. The structural half of the fix is not here
 * — it is .grounds-frame lifting the command rail out of the canvas column,
 * which is width-independent and repairs the toolbar at every size. This block
 * is the rest: give the canvas back the width the chrome was holding.
 *
 * Same 1024 as the two tiers above, and for the same reason — a tablet changes
 * chrome ONCE, at one width, in one rotation, instead of the Grounds collapsing
 * at a number of its own. It also supersedes the old ≤1099px plate rule, so
 * 1025–1099 gets its vellum plates back.
 *
 * The library stays a RAIL here; folding it to a bottom shelf (the §5.9 ladder's
 * original answer) was built and measured and rejected — it traded ~240px of
 * height for width and made the canvas the smallest zone on screen. Measured
 * #floor-stack width, today → after: 800px window 296 → 546, 701px 197 → 447,
 * for the cost of the 45px floor strip.
 * ========================================================================= */
/* Rail-SHAPE rules — scoped as a range, because below 701px the library is not
 * a rail at all but a full-width shelf, where none of this scarcity applies (and
 * applying it stretched the cap meter into a full-width bar and hid a size word
 * the shelf has ample room for).
 *
 * "Below 701px" is no longer the whole of "not a rail" (#283): the tier's height
 * branch folds the library to that same shelf on a landscape phone, which clears
 * the width test. The negation is what keeps this comment's own prediction from
 * coming true there. */
@media (min-width: 701px) and (max-width: 1024px) and (not ((max-height: 500px) and (pointer: coarse))) {
  /* The rail slims to 200px. Chips set the floor, and the size word is what
   * costs: across the 61 catalog facilities, 46 fit on one line at 200px
   * WITHOUT it against 17 with it — so dropping it reads better at 200px than
   * keeping it at 240px. The hard floor is 156px ("Teleportation Circle", the
   * longest unbreakable name). The size is still carried in the chip's title
   * (render-sidebar.js), which appends it at every width. */
  .grounds-aside { flex: 0 0 200px; }
  .grounds-aside-head { padding: 12px 12px 10px; }
  .library-cat { padding: 12px 12px 0; }
  .tray-chip-size { display: none; }

  /* The cap-echo is what blocks any rail under 250px — 222px of min-content on
   * its own, because the count is nowrap and the meter is a fixed 72px. Let the
   * meter drop to its own line and the head fits a 200px column. Scoped to the
   * rail: the same component sits in the Register's header and the Charter's
   * figures grid, where there is no width pressure and it must not reflow. */
  .grounds-aside .cap-echo { flex-wrap: wrap; row-gap: 6px; padding: 6px 8px; }
  .grounds-aside .cap-echo .cap-echo-meter { flex-basis: 100%; width: auto; }
}

@media (max-width: 1024px) {
  /* The caliper seam dissolves; the rail takes a plain hairline instead. The
   * drafting-table signature (spine plates + caliper) is a ≥1025 signature, not
   * a universal one — and nothing informational is lost, since the map carries
   * its own 5-cell scale bar. drawSeamRuler() bails on a zero-size seam and
   * clears stale ticks, so this needs no JS. */
  .grounds-seam { display: none; }
  .grounds-aside { border-left: 1px solid var(--bb-rule-soft); }

  /* The spine folds to a horizontal strip above the canvas: 116px of width for
   * 45px of height, which is the right trade when width is the scarce axis.
   * The picker leads (order:-1) so the strip reads [floor ▾] [+ Floor]
   * [+ Basement]. */
  .stage-body { flex-direction: column; }
  .floor-spine {
    position: static;
    flex: 0 0 auto;
    width: auto;
    flex-direction: row;
    align-items: center;
    background: transparent;
    padding: 0 0 10px;
  }
  .floor-spine-plates { display: none; }
  /* Same 220px as the phone tier's strip below: wherever the spine is a
   * horizontal strip the compact switcher is the same control, so it should not
   * change width between the two tiers that render it that way. */
  .floor-select-wrap { display: block; order: -1; max-width: 220px; }

  /* No spine to clear, so the dock sits at the plate's left edge and may use
   * nearly the full pane. It sheds NO instruments: the #158/#230 shedding order
   * is not a scarcity ladder, it is a statement that phones don't edit (pinch
   * replaces zoom, iOS Safari has no requestFullscreen, and you can't align
   * stairs you can't draw). All three reasons are false here. */
  .grounds-view-dock { left: 20px; max-width: calc(100% - 40px); }

  /* The status zone's constant height is structural now (#275). It spans the
   * chapter, so the hint and the type-pickers share one row at every width in
   * the tier — see the deleted ≤900px rule, which used to wrap the banner by
   * mode and shift the canvas whenever Stairs or Defenses was armed. */
  .placement-banner { flex-wrap: nowrap; }
  .placement-banner .type-pick { flex: 0 0 auto; }
}

/* The chapter nav sheds its ordinals before it overflows (#218). Measured max-
 * content width of the five tabs, by display face: Cartographer (Cinzel) 805px,
 * arcane 727, codex 712, verdant 669 — so between 700 and ~805 the nav, not the
 * top bar, was what pushed the document sideways, and it did it on the one
 * theme whose face is widest. That is the whole of the reported 701–803 band.
 *
 * The numeral is the right thing to spend: it is decorative (aria-hidden) and
 * costs ~80px across all five tabs, which is more than the deficit at every
 * width in this range. The alternative — overflow-x: auto, the phone tier's
 * fallback — was measured and rejected here: it adds a 10px scrollbar gutter
 * and lifts the active tab off the page body, breaking the connected-plaque
 * bridge (+1px → −9px) that is the nav's whole affordance.
 *
 * flex-to-fill comes along because ordinals alone still leave Cinzel ~10px
 * short at 705px; together they clear it with no label clipped. Both levers are
 * the phone tier's own (#158 round 2) — this tier just starts them earlier. */
@media (max-width: 820px) {
  .chapter-tab .numeral { display: none; }
  .chapter-nav .chapter-tab {
    flex: 1 1 auto;
    min-width: 0;
    justify-content: center;
  }
}

/* =========================================================================
 * Phone tier (#105) — the CROSS-SURFACE half
 *
 * What is left here after #298 is not "the rules nobody moved". It is the
 * rules that cannot be moved, and the distinction is worth keeping straight.
 *
 * A tier rule can live beside its component only when its component is the
 * only thing it touches. The Chronicle's roll and writ, Stores, the advance
 * bar and the tab band all cleared that bar and now sit at the end of their
 * own sections. What remains — the shell un-clip, the Charter fold, the
 * Grounds shelf, the top-bar collapse — are single layout DECISIONS spanning
 * many components at once ("on a phone the Grounds becomes one scrolling
 * document"), and their pieces are defined in five different sections. Moving
 * one beside "its" component would land it above a base rule it overrides,
 * which validate_styles.py G6 refuses — correctly, because a media query adds
 * no specificity and the rule would silently stop applying.
 *
 * So: a rule belongs HERE when it coordinates several surfaces, and beside its
 * component when it does not. Adding a single-surface rule to this block is
 * the mistake the contract exists to prevent.
 *
 * The desktop shell is a fixed-height (100vh) grid with two independently
 * scrolling panes and overflow:hidden — on a phone that clips the floorplan
 * below the fold. Here we relax the shell into a single scrolling document,
 * make the floorplan pannable, hide the (desktop-only) editing chrome, and
 * collapse the top bar's secondary actions into the ⋯ More menu. Editing is
 * a desktop affordance; touch-based editing is deferred to #107.
 *
 * TWO BRANCHES since #283 — the tier is DEVICE-shaped, not width-shaped.
 * §5.9 ruling 6 already stated its justification in device terms (pinch replaces
 * zoom, iOS Safari has no requestFullscreen, you can't align stairs you can't
 * draw), so 700px was always a proxy for a predicate about phones. A LANDSCAPE
 * phone is where the proxy leaked: 844×390 on an iPhone 14/15 missed the tier by
 * 144px and got the editing one — 258px of chrome in a 390px viewport, a 152px
 * canvas (5 grid rows), and the view dock covering nearly all of it. Worse, both
 * touch guards in render-svg-nodes.js hang off this same query, so a tap lifted a
 * facility into placement and the hover card opened with no mouseleave to close
 * it. The height branch reclaims 117px in one declaration (.grounds-command
 * carries the toolbar AND the status zone) and leaves 39px pinned.
 *
 * 500 is a DEFINITION, not a device size: once (pointer: coarse) has excluded
 * every fine-pointer window, the number's only job is to split phone landscape
 * (~448 tallest) from tablet landscape (~544 shortest — a 600dp Android tablet
 * with Chrome's toolbar; iPad mini ~660, iPad 10.2 ~730). It is the midpoint of
 * that gap, and both bounds are device facts, so no app measurement can move it.
 * (pointer: coarse) is load-bearing — without it a half-height desktop window
 * would drop to read-only, and that user has a mouse and is editing.
 *
 * SCOPING RULE for anything added below, and for the ≤700px blocks elsewhere in
 * this sheet: a rule that makes content TALLER to fit a narrow column stays
 * width-only (.milestone-line, the homebrew field rows). A rule that makes chrome
 * SHORTER, enlarges a tap target, or re-anchors a popover to a viewport edge
 * belongs to the tier and carries both branches. Keep this query byte-identical
 * to PHONE_TIER_QUERY in constants.js — four JS sites read it, and drift is
 * silent.
 * ========================================================================= */
@media (max-width: 700px), (max-height: 500px) and (pointer: coarse) {
  /* --- Un-clip the shell: one scrolling document, not two fixed panes --- */
  /* grid-template-columns: minmax(0, 1fr) is the load-bearing fix. The
   * .app-shell grid's implicit column is `auto`, which grows to its widest
   * item's min-content — and once render() builds the floorplan SVG (~770px
   * for the default grid), that min-content blew the single column out to
   * ~794px, dragging the top bar and every panel off the right edge (the
   * "looks fine on load, then jumps to overflow" symptom). Pinning the column
   * to minmax(0, 1fr) lets it shrink to the viewport; min-width: 0 down the
   * chain lets the floorplan's width land on .floor-stack, which scrolls it.
   * overflow-x: clip (not hidden — hidden would force overflow-y: auto and make
   * .app-shell a nested vertical scroller, breaking the sticky chapter nav) is
   * a belt-and-braces guard against any residual bleed.
   *
   * display: block (#158): a sticky grid item can't travel inside its own
   * `auto` row, so the pinned chapter-nav below needs the shell to stop being
   * a grid. Block flow is equivalent here — the phone tier is a single
   * min-content-safe column already (grid-template-columns kept as a no-op
   * guard should display:block ever be reverted). */
  .app-shell {
    display: block;
    height: auto;
    min-height: 100dvh;
    grid-template-columns: minmax(0, 1fr);
    overflow-x: clip;
  }
  /* One scrolling document: let the page card + The Grounds flow instead of
   * clipping to internal scroll regions. */
  .chapter-region { overflow: visible; }
  .chapter { overflow: visible; }
  #chapter-grounds { overflow: visible; display: block; }
  /* The Charter's phone block used to live here — ~55 lines against
   * .charter-grid / .charter-body / .figures, none of which survived #301. It
   * was also single-surface, so the band was never its right home; the
   * re-derived rules sit in "Charter · Responsive", last in the Charter's own
   * section. The band keeps only what coordinates several components at once. */
  /* The whole document scrolls on the shelf, not the pane — so .stage reverts to
     a plain block and .stage-scroll stops being a nested scroller (#230 pt1). */
  .chapter-grounds .stage { display: block; overflow: visible; padding: 0 12px 24px; min-width: 0; }
  .chapter-grounds .stage-scroll { overflow: visible; padding: 0; min-height: 0; }
  /* Spine folds to a horizontal strip above the canvas — just the compact
   * #floor-select switcher (plates + add buttons are hidden here). Un-sticky
   * since the whole document scrolls now, not the pane (#160). */
  .stage-body { flex-direction: column; }
  .floor-spine {
    position: static;
    flex: 0 0 auto;
    width: auto;
    flex-direction: row;
    align-items: center;
    background: transparent;
    padding: 0 0 10px;
  }
  .floor-select-wrap { max-width: 220px; }
  .chapter-grounds .chapter-head { padding-left: 12px; padding-right: 12px; }
  /* View-controls cluster wraps instead of running off the right edge —
   * Save SVG was half off-screen (#158 round 2). */
  .grid-ctrls { flex-wrap: wrap; row-gap: 6px; }

  /* Library reflows to a full-width shelf below the canvas (#161): the two
   * columns collapse to a stack, the left seam becomes the dashed shelf-top
   * cue, and Commission unsticks. Chips + Commission stay usable; touch
   * drag-placement remains today's (deferred-to-#160) behavior. */
  .grounds-body { display: block; }
  /* The caliper seam dissolves into the aside's dashed shelf-top border. */
  .grounds-seam { display: none; }
  .grounds-aside {
    flex: none;
    width: auto;
    overflow: visible;
    border-left: none;
    border-top: 1px dashed var(--bb-rule);
  }
  .grounds-aside-head { position: static; }
  /* Footer follows the head off its sticky pin on the shelf: it becomes the
   * last stacked block with its top hairline, no bottom-edge pinning. */
  .grounds-aside .grounds-since { position: static; margin-top: 0; }

  /* #218 — the prop palette is the aside's twin and needs the same treatment.
   * It had no rule here at all and only worked by accident, because
   * `.grounds-body { display: block }` makes its `flex: 0 0 300px` inert. Its
   * head kept `position: sticky` inside a container that no longer scrolls,
   * pinning it to nothing; the palette itself kept `overflow-y: auto`, so a
   * 74-prop register scrolled INSIDE a short box instead of with the page. */
  .furnish-palette,
  body[data-edit-mode="furnishings"] .furnish-palette {
    flex: none;
    width: auto;
    overflow: visible;
    border-top: 1px dashed var(--bb-rule);
  }
  .furnish-palette-head { position: static; }
  .furnish-group-label { position: static; }

  /* Sticky editing bars would stack under the sticky top bar once the document
   * (not the pane) scrolls — un-stick them. */
  .grounds-command { position: static; }

  /* View-dock sheds its float slab and flows inline below the canvas as a static
   * row (sticky can't pin once the document, not the pane, scrolls). On phones
   * the zoom cluster / Fit / fullscreen / ghost are already display:none (#158,
   * below); grid-dims + seams go too, leaving Overview as a thumb-sized target. */
  .grounds-view-dock {
    position: static;
    width: auto;
    /* The ≤1024px block still matches on a landscape phone and sets
     * max-width: calc(100% - 40px) to keep the FLOATING dock off the plate's
     * right edge. `width: auto` doesn't override a max-width, so without this
     * the un-floated flow row would sit 40px narrow for no reason (#283). */
    max-width: none;
    padding: 8px 0 0;
    background: transparent;
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
    border: 0;
    box-shadow: none;
  }
  .grounds-view-dock .grid-dims,
  .grounds-view-dock .grid-ctrls-seam { display: none; }
  #btn-overview { min-height: 44px; }

  /* --- Floorplan: view + pan only --- */
  .floor-stack {
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
    /* `pinch-zoom` must be listed EXPLICITLY: `pan-x pan-y` alone disables the
     * browser's pinch gesture over this element, which killed the one zoom the
     * phone tier has (#158 removed the zoom buttons precisely because pinch is
     * the native gesture). The symptom was pinching working everywhere on the
     * page EXCEPT on the canvas you actually want to zoom. */
    touch-action: pan-x pan-y pinch-zoom;
  }

  /* --- Hide desktop-only editing chrome (display:none, not DOM removal, so
   * landscape/resize restores it without JS). Keep Save SVG + Overview —
   * they're view/export actions. The zoom cluster + fullscreen go too (#158
   * round 3): the canvas auto-fits the screen width and pinch-zoom is the
   * native gesture from there — a "reset to 100%" tap is meaningless on a
   * phone, and iPhone Safari has no requestFullscreen at all. --- */
  .grounds-command,
  .spine-add-btn,
  .plate-menu-btn,
  .grid-edge-handles-layer,
  #btn-ghost-underlay,
  #btn-fit-grid,
  .chapter-grounds .zoom-ctrl,
  #btn-fullscreen { display: none; }

  /* --- Top bar: the ⋯ fold begins at the tablet tier above (#218) — this block
   * folds two more things. Open handout (the last verb standing; here the band
   * IS scarce) and the autosave colophon (it reclaims the scarce band; saves
   * are still managed from Bastions ▾). Bastions ▾ stays at every width
   * (#121 §5.2). --- */
  #btn-export-handout,
  .top-bar-colophon { display: none; }
  /* The VTT battle map is a prep-desk act, not a table one — the tablet tier's
   * ⋯ carries it, the phone's does not (the #230 pt1 ruling, previously
   * expressed by offering only the SVG row at all widths). */
  #more-menu [data-more="export-map-vtt"] { display: none; }
  /* Turn cartouche (#112) STAYS on phones — turn state is play state, and the
   * hidden colophon vacates exactly this slot. Sheds to a minimal "Turn 3"
   * pill (caret dropped); never folded into ⋯ More. */
  .turn-cartouche { padding: 4px 8px; }
  .turn-cartouche .turn-caret { display: none; }

  /* Dropdown panels: never wider than the screen. Triggers are left-aligned on
   * narrow, so keep the base left:0 anchor (opens rightward) and just cap the
   * width — the base min-width:340px would otherwise risk spilling off-screen. */
  .bastions-menu {
    min-width: 0;
    width: max-content;
    max-width: calc(100vw - 24px);
  }
  /* #more-menu keeps its desktop right:0 anchor: the ⋯ trigger trails at the
   * right edge of the (now single-line, #158 round 2) bar, so the panel opens
   * leftward into the screen. (The old left:0 re-anchor served the two-row
   * layout where the trigger sat flush left — kept it would now push the
   * panel off the right edge.) */

  /* Top bar compacts to ONE line: the wordmark drops, the sigil stays as a
   * compact app mark, and the spacer keeps its desktop flex:1 role so the ⋯
   * button trails right on the same row (the old forced wrap gave ⋯ More a
   * whole second band — #158 round 2). */
  .top-bar {
    padding: 8px 14px;
    flex-wrap: wrap;
    gap: 8px 10px;
  }
  .top-bar-brand .wordmark { display: none; }

  /* scrollIntoView targets clear the pinned bar: the cartouche jump, the turn
   * sheet's focusCard, Register's spotlight, and the Map-key disc reveal. */
  .turn-fac-card,
  .chronicle-section,
  .reg-card,
  .floor-svg-wrap {
    scroll-margin-top: 56px;
  }


}

/* =========================================================================
 * Landscape-phone corrections (#283) — inside the tier, but WIDE.
 *
 * The tier above was written against a 375px column. Everything in it either
 * still reads correctly at 828px or is merely airy (the Charter's 2-up figures,
 * the stacked Stores rows — both accepted; the Help dialog's hidden nav rail is
 * actively MORE correct here, since the dialog is boxed at 88vh = 343px and a
 * 20-section rail in that box is useless).
 *
 * The exception is the three bottom sheets. All of them pin left AND right, so
 * they go the full 828px: a 273px-tall Map display panel, SRD prose at a
 * ~120-character measure, five name candidates strung across the screen. Cap and
 * centre them. On a `position: fixed` element with both offsets set, clamping
 * width leaves the remainder to `margin-inline: auto`, which centres it under
 * the over-constrained-resolution rules — no other change needed.
 *
 * Anchoring them instead (dropping them out of the tier and letting the JS place
 * them) was rejected: at 390px tall the clamp in positionCard() pins a ~300px
 * card to top:8, covering nearly the whole screen with no edge and no visible
 * relationship to the word that was tapped. A capped sheet has both.
 * ========================================================================= */
@media (max-height: 500px) and (pointer: coarse) and (min-width: 701px) {
  .map-display-panel,
  .ref-card--sheet,
  .name-suggest-card--sheet {
    max-width: 520px;
    margin-inline: auto;
  }
  /* The tier makes Advance a full-width slab so it is unmissable. At 828px a
   * full-bleed gilt bar stops reading as a button and starts reading as the
   * snapshot-band idiom — a grammar collision. Cap it: still the widest thing in
   * the footer, still terminal, no longer a banner. */
  .turn-footer-actions .turn-advance { max-width: 420px; }
}

/* ---- D&D Beyond import dialog (#148) ------------------------------------ */

/* Step rail: Roman ordinals matching the chapter-nav numeral language */
.ddb-steps {
  display: flex;
  gap: 0;
  list-style: none;
  margin: -6px 0 2px;
  padding: 0;
  border-bottom: 1px solid var(--bb-rule-soft);
}
.ddb-step {
  padding: 5px 14px 5px 0;
  font-family: var(--bb-font-display);
  font-size: 0.75rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--bb-ink-faint);
}
.ddb-step.is-active { color: var(--bb-vermillion); }

/* Panel headings and intro copy */
.ddb-panel-heading {
  margin: 0 0 6px;
  font-family: var(--bb-font-display);
  font-size: 0.9375rem;
  color: var(--bb-ink);
}
.ddb-panel-sub {
  margin: 0 0 10px;
  font-family: var(--bb-font-italic);
  font-style: italic;
  font-size: 0.875rem;
  color: var(--bb-ink-soft);
}
.ddb-panel-sub kbd {
  font-family: var(--bb-font-text);
  font-style: normal;
  font-size: 0.875em;
  background: var(--bb-parchment-mid);
  border: 1px solid var(--bb-rule-soft);
  padding: 0 4px;
}

/* Step I: URL input + service URL block */
.ddb-url-input {
  display: block;
  width: 100%;
  box-sizing: border-box;
  background: var(--bb-parchment-light);
  border: 1px solid var(--bb-rule-soft);
  border-radius: 0;
  padding: 6px 10px;
  font-family: var(--bb-font-text);
  font-size: 0.875rem;
  color: var(--bb-ink);
  box-shadow: none;
}
.ddb-url-input:focus { outline: none; border-color: var(--bb-vermillion); }
.ddb-service-block {
  margin-top: 8px;
  padding: 8px 10px;
  background: var(--bb-parchment);
  border: 1px solid var(--bb-rule-soft);
}
.ddb-url-code {
  display: block;
  font-family: var(--bb-font-text);
  font-size: 0.8125rem;
  color: var(--bb-ink-soft);
  word-break: break-all;
  margin-bottom: 6px;
  user-select: all;
}
.ddb-url-actions {
  display: flex;
  gap: 14px;
}
.ddb-inline-btn {
  background: none;
  border: none;
  padding: 0;
  font-family: var(--bb-font-italic);
  font-style: italic;
  font-size: 0.875rem;
  color: var(--bb-vermillion);
  text-decoration: underline dotted;
  text-underline-offset: 2px;
  cursor: pointer;
}
.ddb-inline-btn:hover { color: var(--bb-vermillion-deep); }
a.ddb-inline-btn { display: inline; }

/* Step II: JSON textarea + parse error */
.ddb-json-input { margin-bottom: 0; }
.ddb-parse-error {
  margin: 6px 0 0;
  font-family: var(--bb-font-italic);
  font-style: italic;
  font-size: 0.875rem;
  color: var(--bb-danger);
}

/* #204 — muted "live fetch didn't work out" notice atop the paste step. A
   gilt hairline, not the red error voice: the manual paste path is the plan B
   the user was always promised, not a failure state. */
.ddb-fetch-notice {
  border-left: 3px solid var(--bb-gilt);
  padding-left: 10px;
}

/* Sync entries land straight on Review — this quiet link is the way back to
   step I for pointing the owner at a different character. */
.ddb-relink { margin-top: 6px; }

/* Step III: preview fields */
.ddb-preview-fields {
  display: grid;
  gap: 0;
  margin: 0 0 12px;
}
.ddb-preview-row {
  display: grid;
  grid-template-columns: 5rem 1fr;
  gap: 4px;
  padding: 5px 0;
  border-bottom: 1px solid var(--bb-rule-soft);
}
.ddb-preview-row:first-child { border-top: 1px solid var(--bb-rule-soft); }
.ddb-preview-row dt {
  font-family: var(--bb-font-display);
  font-size: 0.8125rem;
  color: var(--bb-ink-faint);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  align-self: center;
}
.ddb-preview-row dd {
  margin: 0;
  font-family: var(--bb-font-text);
  font-size: 0.9375rem;
  color: var(--bb-ink);
  align-self: center;
}

/* Focus-type fieldset */
.ddb-focus-group {
  border: none;
  margin: 0 0 10px;
  padding: 0;
}
.ddb-focus-legend {
  font-family: var(--bb-font-display);
  font-size: 0.8125rem;
  color: var(--bb-ink-faint);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  margin-bottom: 4px;
  padding: 0;
}
.ddb-focus-sub {
  font-family: var(--bb-font-italic);
  font-style: italic;
  font-size: 0.8125rem;
  color: var(--bb-ink-soft);
  margin: 0 0 8px;
}
.ddb-focus-options {
  display: flex;
  flex-direction: column;
  gap: 6px;
}
.ddb-focus-hint {
  font-family: var(--bb-font-italic);
  font-style: italic;
  font-size: 0.8125rem;
  color: var(--bb-ink-faint);
  margin-left: 4px;
}
/* URL echo block in step II — same chrome as step I's .ddb-service-block */
.ddb-step-ii-echo { margin-bottom: 8px; }

/* Focus checkboxes reuse the settings-body .view-toggle recipe */
#ddb-import-dialog .view-toggle {
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: 8px;
  font-size: 0.875rem;
  color: var(--bb-ink);
  cursor: pointer;
  user-select: none;
}
#ddb-import-dialog .view-toggle input {
  position: absolute; width: 1px; height: 1px;
  margin: -1px; padding: 0; overflow: hidden;
  clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}
#ddb-import-dialog .view-toggle::before {
  content: ''; display: inline-block;
  width: 18px; height: 18px; flex: 0 0 18px;
  border: 1px solid var(--bb-rule);
  background: var(--bb-parchment-light);
  border-radius: 0; box-sizing: border-box;
}
#ddb-import-dialog .view-toggle:has(input:checked)::before {
  background: var(--bb-vermillion-deep);
  border-color: var(--bb-vermillion-deep);
}
#ddb-import-dialog .view-toggle::after {
  content: ''; position: absolute;
  left: 5px; top: 50%;
  width: 5px; height: 10px;
  border-right: 2px solid var(--bb-parchment-light);
  border-bottom: 2px solid var(--bb-parchment-light);
  transform: translateY(-65%) rotate(45deg);
  opacity: 0; pointer-events: none;
}
#ddb-import-dialog .view-toggle:has(input:checked)::after { opacity: 1; }
#ddb-import-dialog .view-toggle:has(input:focus-visible)::before {
  box-shadow: 0 0 0 2px var(--bb-vermillion);
}

/* Caveats list — gilt advisory, bulletless */
.ddb-caveats {
  list-style: none;
  margin: 2px 0 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 4px;
}

/* .dialog-warn inside the DDB dialog panels (outside .dialog-body) */
#ddb-import-dialog .dialog-warn {
  display: block;
  font-family: var(--bb-font-italic);
  font-style: italic;
  color: var(--bb-gilt-deep);
}

/* =========================================================================
 * #209 — Shared-snapshot read-only viewer
 * ========================================================================= */

/* The app shell is a 3-row grid (top-bar · nav · region = auto auto 1fr). In the
 * viewer the snapshot banner becomes a 4th grid child, so add its (auto) row —
 * otherwise the banner would grab the region's 1fr and balloon to fill the page. */
body.viewer .app-shell {
  grid-template-rows: auto auto auto 1fr;
}

/* Passing through Settings from home with an empty library (#268). The boot
 * mints an untitled bastion for the dialog to sit over, and a DM who came to
 * back up a shelf or connect cloud sync has no business seeing an editable
 * phantom that looks like a bastion of theirs. Hide the shell rather than
 * suppress the boot: the dialog needs a live session behind it, and app.js
 * returns them to home on close. */
/* Every `<dialog>` in index.html is a SIBLING of .app-shell, not a descendant,
 * so hiding the shell cannot hide them and no companion un-hide rule is needed.
 * (One was written and removed: it was dead weight, and scoping it to
 * #settings-dialog would have been dead weight with a hazard attached, since it
 * implies the others are being suppressed when they never were.) */
body.settings-passthrough .app-shell { visibility: hidden; }

/* The banner shown only in the viewer (body.viewer). A full-bleed gilt-wash
 * band with hairline top/bottom rules — deliberately NOT the gilt-left-accented
 * .compliance-notice card nor the vermillion Register alert, so "you're holding
 * a borrowed snapshot" never reads as "something is wrong". */
.snapshot-banner {
  display: flex;
  align-items: center;
  /* Wraps at every width, not just the phone tier: a fourth control makes the
   * action row too wide to sit beside the message on a narrow laptop, and
   * wrapping the row onto its own line is what the ≤720px tier already did by
   * hand. Flexbox wraps before it shrinks, so the message keeps its natural
   * measure instead of being squeezed to one word. No new breakpoint. */
  flex-wrap: wrap;
  gap: 12px;
  padding: 10px 20px;
  background: color-mix(in srgb, var(--bb-gilt) 12%, var(--bb-parchment-light));
  border-top: 1px solid var(--bb-gilt-deep);
  border-bottom: 1px solid var(--bb-gilt-deep);
  color: var(--bb-ink);
}
.snapshot-seal {
  width: 26px;
  height: 26px;
  flex: 0 0 auto;
  background-color: var(--bb-gilt-deep);
  -webkit-mask: url(icons/wax-seal.svg) center / contain no-repeat;
  mask: url(icons/wax-seal.svg) center / contain no-repeat;
}
.snapshot-msg {
  margin: 0;
  font-family: var(--bb-font-italic);
  font-style: italic;
  font-size: 0.9375rem;
  color: var(--bb-ink-soft);
}
.snapshot-name {
  font-weight: 600;
  color: var(--bb-ink);
}
.snapshot-actions {
  margin-left: auto;
  display: flex;
  /* Once the row is on a line of its own it can still be wider than the band,
   * so it wraps inside itself too. `flex-shrink: 1` is what lets that happen:
   * with the old `0 0 auto` the group could not be squeezed, wrap never
   * engaged, and the band simply ran off the page — 258px of overflow at
   * 760px once a fourth control joined. */
  flex-wrap: wrap;
  gap: 8px;
  flex: 0 1 auto;
}
/* An intentional three-tier set: gilt primary (Download), neutral parchment
 * secondary (Save a copy — the base button chrome), text-like ghost (Leave).
 * .btn-ghost's transparent chrome is .top-bar-scoped, so restate it here or the
 * ghost would render as a solid button like the secondary. */
.snapshot-actions button {
  font-family: var(--bb-font-display);
  font-size: 0.8125rem;
  letter-spacing: 0.05em;
  padding: 6px 14px;
}
/* DONE, not unavailable (#268). A completed act must never wear the disabled
 * treatment. `#2a1d0c` at the house 0.4 dim composites to 2.66:1 over this gilt
 * band at 13px/500 — the confirmation of the act would be the faintest text in
 * the banner — and `disabled` drops the control from the tab order and announces
 * "unavailable", the opposite of what happened. WCAG exempts inactive controls
 * from 1.4.3; a status message in a disabled control's clothes borrows an
 * exemption it isn't entitled to. Full ink, gilt check, no opacity: the mark
 * carries the state and the contrast stays legible. `aria-disabled` keeps it
 * focusable, so its tooltip has a keyboard path.
 * Nothing in this row is dimmed any more — the Roll20-Pro control that was
 * (greyed because it was out of ORDER, not missing) went with the two-step. So
 * `opacity: 1` is a guard, not a fix: it exists to make a future blanket dim on
 * this row fail loudly here rather than quietly re-create the 2.66:1. */
.snapshot-actions button.is-done {
  cursor: default;
  opacity: 1;
}
.snapshot-actions button.is-done .shelf-done-mark {
  color: var(--bb-gilt-deep);
  font-weight: 700;
}
.snapshot-actions .btn-ghost {
  background: transparent;
  border-color: transparent;
  box-shadow: none;
  color: var(--bb-ink-soft);
}
.snapshot-actions .btn-ghost:hover {
  background: transparent;
  border-color: var(--bb-rule-soft);
  color: var(--bb-ink);
}
@media (max-width: 720px) {
  .snapshot-actions { margin-left: 0; width: 100%; }
  /* Touch targets: the row's 6px/0.8125rem chrome measured ~30px tall. */
  .snapshot-actions button { min-height: 44px; padding: 10px 14px; }
}

/* Read-only hide-set — the belt-and-braces layer behind the render-time
 * omissions (render-*.js). Chrome that only makes sense for an owner editing
 * their own library disappears; the view-only affordances (zoom, Overview,
 * Export map ▾, floor switching, Open handout, Help) stay. */
/* :not(.export-map-wrap) — Export map ▾ moved into .top-bar as a .bastions-menu-wrap
   (#230 pt1) and is a view-only affordance that STAYS in the viewer (per the note
   above); only the Bastions ▾ switcher wrapper should hide here. */
body.viewer .top-bar .bastions-menu-wrap:not(.export-map-wrap),
body.viewer #turn-cartouche,
body.viewer #save-status,
body.viewer #btn-settings,
body.viewer #more-menu [data-more="settings"],
/* #tab-chronicle used to hide here (#209): the chapter's three sections were
   milestones, turns and stores, and the payload emptied all three, so the tab
   led to nothing. #254 gives Stores real content — how the bastion actually
   stands — so the tab comes back, and render-history.js hides the two sections
   that are still history. Restore the hide here if that ever reverses. */
body.viewer .grounds-command,
body.viewer .grounds-aside,
body.viewer .grounds-seam,
body.viewer #btn-fit-grid,
body.viewer #btn-ghost-underlay,
body.viewer .spine-add-btn,
/* Charter figures + credentials, Household roster — the mutation buttons that
 * render-time guards don't already omit. Their fields are frozen by
 * lockViewerFields(); these are the buttons. */
body.viewer .btn-add-char,
body.viewer .btn-remove-char,
body.viewer .cred-add,
body.viewer .cred-chip-remove,
body.viewer .house-add,
body.viewer .house-remove,
/* Charter actions that start tracking, share, or import — all owner-only
 * mutations the viewer must not reach (#209). The colophon's verbs are already
 * omitted at render time (renderCharterSurvey gates on state.readOnly); this is
 * the belt behind that, on the same reasoning as the buttons above. The survey
 * and plate DOORS are deliberately absent from this set — a viewer has all five
 * chapters, and looking is what it is for. */
body.viewer .sv-verb,
body.viewer .btn-ddb-import {
  display: none !important;
}
/* The crest and owner portraits keep showing, but their pickers are owner-only
 * editors — freeze the click without hiding the artwork. */
body.viewer #crest-slot,
body.viewer .owner-portrait { pointer-events: none; }
/* …and an empty slot stops advertising itself (#272). The dashed border and the
   pointer cursor are the "click to upload" affordance; on a stranger's card they
   are an invitation the reader cannot accept, which is the same read-only lock
   gap the empty states were fixed for. Solid, so it reads as a frame around a
   portrait nobody supplied. */
body.viewer .owner-portrait.is-placeholder {
  border-style: solid;
  cursor: default;
}
/* The CREST placeholder is the same case, mirrored at last (#218). It stayed
   cheap while the crest sat below the fold on a phone; the masthead puts it at
   the top of a stranger's page. `pointer-events: none` above stops the mouse
   and nothing else — Enter or Space on a focused button still reaches the
   handler — so heraldry.js also sets `disabled` and drops the upload tip in a
   viewer. This is the dashed-border half of it. */
body.viewer #crest-slot.is-placeholder {
  border-style: solid;
  cursor: default;
}
/* The `disabled` half has a cost the placeholder rule above does not: the
   sheet's global `button:disabled` washes the whole button to 45%, artwork
   included, so a reader's copy of a REAL crest printed as faded as the
   placeholder it is not. The artwork keeps showing — that is the sentence at
   the head of this block — and the placeholder's own 45% on its `img` is
   untouched by this. Found in the v3.9.4 pre-tag review. */
body.viewer #crest-slot:disabled,
body.viewer .owner-portrait:disabled { opacity: 1; }

/* Export map ▾ dropdown (#215) reuses .bastions-menu, which is left-anchored and
 * 340px wide for the top-bar saves list. Here it sits at the right edge of the
 * Grounds view-controls with only two short rows, so right-align it and let it
 * size to its content — otherwise it overflows the window and looks half-empty. */
#export-map-menu {
  left: auto;
  right: 0;
  min-width: 0;
  white-space: nowrap;
}
#export-map-menu .bastions-menu-row {
  display: block;
  width: 100%;
  text-align: left;
  padding: 8px 16px;
  background: transparent;
  border: none;
  border-radius: 0;
  color: var(--bb-ink);
  font-family: var(--bb-font-display);
  font-size: 0.875rem;
  letter-spacing: 0.02em;
  cursor: pointer;
  box-shadow: none;
}
#export-map-menu .bastions-menu-row:hover { background: var(--bb-parchment-mid); }

/* Share dialog (#209) — read-only link field + copy, plus the crest/length
 * caveat notes and the slim-link offer. */
.share-link-row {
  display: flex;
  gap: 8px;
  margin: 4px 0 8px;
}
.share-link-input {
  flex: 1 1 auto;
  min-width: 0;
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 0.8125rem;
  padding: 6px 8px;
  border: 1px solid var(--bb-rule);
  border-radius: 3px;
  background: var(--bb-parchment-light);
  color: var(--bb-ink-soft);
}
.linklike {
  background: none;
  border: none;
  /* The base `button` rule's raised treatment is a box-shadow, which clearing
   * background and border does not touch — so without this a .linklike carries
   * a faint plaque behind its underline at every width, which is the opposite
   * of what the class is for. */
  box-shadow: none;
  padding: 0;
  color: var(--bb-gilt-deep);
  font: inherit;
  text-decoration: underline;
  cursor: pointer;
}
.linklike:hover { color: var(--bb-gilt); }
/* The house ring, not the UA's. Vermillion rather than gilt-deep, which is the
 * other focus ink in this sheet: .linklike's resting colour IS gilt-deep, so
 * that ring would sit at low contrast against the control's own text. It also
 * matches every immediate neighbour a .linklike has — .home-foot-btn beside it
 * on the shelf rail, and .dialog-confirm beside it in the Discord dialog. */
.linklike:focus-visible {
  outline: 2px solid var(--bb-vermillion);
  outline-offset: 2px;
}

/* =========================================================================
 * #303 — Sync-conflict band & concordance
 * ========================================================================= */
/* Two of the player's own devices wrote to one bastion since they last agreed.
 * The band is the NOTICE (standing, in every chapter, until settled); the
 * concordance inside the choice modal is the ACT.
 *
 * Colour ruling: gilt ground, vermillion mark. Vermillion is this app's
 * compliance channel — a vermillion BAND would make the plumbing read as the
 * keep being out of compliance, which is a statement about the fiction. So the
 * ground borrows .snapshot-banner's gilt wash and only the seal is vermillion,
 * which is the same division the Register's alert dot already uses. */

/* The shell is a 3-row grid (top-bar · nav · region = auto auto 1fr) and the
 * band is a 4th child, so it needs its own auto row — exactly as body.viewer
 * does above for the snapshot banner. Without it the band takes the region's
 * 1fr and balloons to fill the page. The two classes never co-occur:
 * refreshConflictBand refuses to paint inside the viewer. */
body.sync-conflict .app-shell {
  grid-template-rows: auto auto auto 1fr;
}

/* Never over somebody else's snapshot. refreshConflictBand refuses to paint in
 * the viewer, but entering one does not repaint the band — `enterViewer` runs no
 * sync — so a band already on screen would ride into a session it says nothing
 * about, above a document it does not describe. Held in CSS rather than by
 * adding a call to `enterViewer`: this way it cannot be lost by a future entry
 * path that forgets, and leaving the viewer restores the band by itself. */
body.viewer #sync-conflict-band {
  display: none;
}

.sync-conflict-band {
  display: flex;
  align-items: center;
  /* The verb sits WITH the sentence (no margin-left:auto), so it never drifts
   * ~700px from the clause explaining it at a wide window. Wrapping is still
   * allowed for a long bastion name at a narrow one. */
  flex-wrap: wrap;
  gap: 12px;
  padding: 10px 20px;
  background: color-mix(in srgb, var(--bb-gilt) 12%, var(--bb-parchment-light));
  border-top: 1px solid var(--bb-gilt-deep);
  border-bottom: 1px solid var(--bb-gilt-deep);
  color: var(--bb-ink);
}

/* The double-seal mark. Its geometry is inline in index.html, where the reason
 * for the two discs and the knockout token is recorded; only the size and the
 * ink are set here. */
.sync-conflict-seal {
  flex: 0 0 auto;
  width: 20px;
  height: 20px;
  /* The discs are `fill: currentColor`, so the mark's colour is set once here
   * and the knockout ring stays independent of it. */
  color: var(--bb-vermillion);
}

.sync-conflict-msg {
  margin: 0;
  /* `0 1 auto`, NOT a growing `1 1 240px`: a message that grows fills the band
   * and pushes the verb to the right margin, which measured 1324px from the
   * left at a 1440px window — the button ends up ~700px from the clause that
   * explains it. It still shrinks and wraps, it just doesn't claim the slack. */
  flex: 0 1 auto;
  font-family: var(--bb-font-italic);
  font-style: italic;
  font-size: 0.9375rem;
  color: var(--bb-ink-soft);
}
.sync-conflict-name {
  font-weight: 600;
  font-style: normal;
  color: var(--bb-ink);
}
/* Matches .snapshot-actions' display face and measure, so the two bands speak
 * in one voice on the rare deployment where a reader sees both in a session. */
.sync-conflict-band .btn-primary {
  flex: 0 0 auto;
  font-family: var(--bb-font-display);
  font-size: 0.8125rem;
  letter-spacing: 0.05em;
  padding: 6px 14px;
}

/* ---- the concordance, inside the choice modal --------------------------- */

/* What each copy actually holds, side by side. The reader is being asked to
 * destroy one copy of their own campaign; two timestamps are not evidence
 * enough to do that on. */
/* Named for this dialog, not `.dialog-lede`: a generic primitive defined inside
 * one feature's section is a rule the next author reuses and this section then
 * silently owns. If a second dialog ever wants one, promote it to the modal
 * rules deliberately rather than by accident. */
.sync-lede {
  margin: 0 0 10px;
  font-family: var(--bb-font-italic);
  font-style: italic;
  color: var(--bb-ink-soft);
}
.sync-concordance {
  width: 100%;
  border-collapse: collapse;
  margin-bottom: 4px;
}
.sync-concordance th[scope="col"] {
  text-align: left;
  padding: 0 0 6px;
  font-family: var(--bb-font-display);
  font-size: 0.8125rem;
  letter-spacing: 0.06em;
  font-weight: 600;
  color: var(--bb-ink);
  border-bottom: 1px solid var(--bb-gilt-deep);
}
/* The "edited here 2 hours ago" line under each leaf's name — the only fact
 * the app has that distinguishes the two devices, since it holds no device
 * identity of any kind. */
.sync-concordance th[scope="col"] span {
  display: block;
  font-family: var(--bb-font-italic);
  font-style: italic;
  font-weight: 400;
  font-size: 0.8125rem;
  letter-spacing: 0;
  color: var(--bb-ink-soft);
}
/* A hairline gutter between the leaves, not a rule around every cell: this is
 * a spread, the same figure the Charter and the Chronicle already use. */
.sync-concordance td:nth-child(2),
.sync-concordance th[scope="col"]:first-of-type {
  border-right: 1px solid var(--bb-gilt-deep);
  padding-right: 16px;
}
.sync-concordance th[scope="col"]:last-of-type,
.sync-concordance td:last-child {
  padding-left: 16px;
}
.sync-concordance th[scope="row"] {
  text-align: left;
  font-weight: 400;
  font-family: var(--bb-font-italic);
  font-style: italic;
  color: var(--bb-ink-soft);
  padding: 4px 12px 4px 0;
}
.sync-concordance td {
  text-align: right;
  font-variant-numeric: tabular-nums;
  padding: 4px 0;
  color: var(--bb-ink);
}
/* Rows the two copies AGREE on recede; rows they differ on keep full ink.
 * WEIGHT, never colour: an early pass underlined the differing values in
 * vermillion and read instantly as a compliance breach. ink-faint measures
 * 5.74:1 on this ground against full ink's 14.29:1 — both clear of AA, so the
 * recession is a reading order and never a legibility floor. */
.sync-concordance .is-same th[scope="row"],
.sync-concordance .is-same td {
  color: var(--bb-ink-faint);
}

/* ---- Responsive (phone tier) -------------------------------------------- */

/* Single-surface, so it sits beside its surface rather than in the
 * cross-surface band — and LAST in this section, after every base rule it
 * overrides, since a media query adds no specificity and source order is the
 * whole of the cascade decision here. */
@media (max-width: 700px), (max-height: 500px) and (pointer: coarse) {
  .sync-conflict-band {
    display: block;
    padding: 8px 12px;
  }
  /* The seal floats into the first line rather than taking a row of its own —
   * a two-row band above the chapter on a phone is most of the fold. */
  .sync-conflict-seal {
    float: left;
    margin: 3px 8px 0 0;
  }
  .sync-conflict-msg {
    font-size: 0.875rem;
  }
  .sync-conflict-band .btn-primary {
    display: block;
    margin-top: 8px;
    /* The coarse-pointer 44px floor: this is the only route out of the state,
     * so it is the last control in the app that may be hard to hit. */
    min-height: 44px;
  }
  /* The spread HOLDS at 390px — it is three narrow columns, not two leaves, so
   * there is nothing to stack. What it needed instead was measured: the gutter
   * padding halves, and both the leaf headings and the row labels drop a step,
   * because "Orders in flight" and "Build milestones" are the two longest
   * labels in the table and are what set its minimum. */
  .sync-concordance td:nth-child(2),
  .sync-concordance th[scope="col"]:first-of-type {
    padding-right: 8px;
  }
  .sync-concordance th[scope="col"]:last-of-type,
  .sync-concordance td:last-child {
    padding-left: 8px;
  }
  .sync-concordance th[scope="col"] {
    font-size: 0.75rem;
  }
  .sync-concordance th[scope="row"] {
    font-size: 0.8125rem;
    padding-right: 6px;
  }
  /* A label that still wraps takes its figure to the BOTTOM of the row, a line
   * below the word it belongs to and level with the next row's label — which
   * reads as the number belonging to the wrong fact. Top-align both so the
   * figure always sits on the label's first line. */
  .sync-concordance th[scope="row"],
  .sync-concordance td {
    vertical-align: top;
  }
}

/* =========================================================================
 * Homebrew (#146)
 * ========================================================================= */
/* The facility editor dialog, the picker's authoring affordances, and the
 * NEW .field-error convention (aria-invalid + vermillion border + a short
 * message under the field — first ratified here; precedent for the border
 * is .turn-event-roll[aria-invalid]). Provenance is ink-soft — a neutral
 * third channel, never vermillion (earned) or gilt (active/reference). */

/* Picker rows: quiet provenance tag + the trailing authoring verb. */
.picker-list .pick-provenance {
  margin-left: 8px;
  color: var(--bb-ink-soft);
  letter-spacing: 0.03em;
}
.pick-fork {
  flex: 0 0 auto;
  align-self: center;
  width: 44px;
  height: 44px;
  margin: -6px 0;                /* stay inside the row's visual height */
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: none;
  border: 1px solid transparent;
  border-radius: 0;
  color: var(--bb-ink-soft);
  cursor: pointer;
  opacity: 0;                    /* revealed on row hover / keyboard focus */
  transition: opacity 0.12s, border-color 0.12s;
}
.picker-list li:hover .pick-fork,
.picker-list li:focus-within .pick-fork { opacity: 1; }
.pick-fork:hover { border-color: var(--bb-rule); background: var(--bb-parchment-mid); }
.pick-fork:focus-visible { opacity: 1; outline: 2px solid var(--bb-vermillion); outline-offset: -2px; }
.pick-fork .glyph-svg { width: 18px; height: 18px; }
@media (pointer: coarse) {
  .pick-fork { opacity: 1; }     /* no hover on touch — always a real target */
}

/* The editor form: one scrolling column of fieldsets inside a bounded dialog. */
#homebrew-editor-dialog .hb-form {
  display: flex;
  flex-direction: column;
  max-height: 88vh;
}
.hb-form-scroll {
  overflow-y: auto;
  padding-right: 12px;           /* scroll gutter, house rule */
  scrollbar-gutter: stable;
  display: flex;
  flex-direction: column;
  gap: 14px;
}
.hb-fieldset {
  border: 1px dashed var(--bb-rule-soft);
  background: var(--bb-parchment-mid);
  padding: 10px 12px 12px;
  margin: 0;
  display: flex;
  flex-direction: column;
  gap: 10px;
}
.hb-fieldset legend {
  font-family: var(--bb-font-display);
  font-size: 0.875rem;
  letter-spacing: 0.04em;
  color: var(--bb-ink-soft);
  padding: 0 6px;
}
.hb-field { display: flex; flex-direction: column; gap: 4px; }
.hb-field-row { display: flex; gap: 12px; }
.hb-field-row .hb-field { flex: 1 1 0; }
.hb-field > label,
.hb-field-label {
  font-family: var(--bb-font-display);
  font-size: 0.8125rem;
  color: var(--bb-ink-soft);
}
.hb-optional {
  font-family: var(--bb-font-italic);
  font-style: italic;
  letter-spacing: 0;
  color: var(--bb-ink-faint);
}
.hb-form input[type="text"],
.hb-form input[type="number"],
.hb-form select,
.hb-form textarea {
  width: 100%;
  box-sizing: border-box;
  font-family: var(--bb-font-text);
  font-size: 0.9375rem;
  color: var(--bb-ink);
  background: var(--bb-parchment-light);
  border: 1px solid var(--bb-rule);
  border-radius: 0;
  padding: 7px 9px;
}
.hb-form textarea { resize: vertical; }
.hb-form input:focus,
.hb-form select:focus,
.hb-form textarea:focus {
  outline: none;
  border-color: var(--bb-vermillion);
}

/* The .field-error convention (new): invalid field + message line. */
.hb-form :is(input, select, textarea)[aria-invalid="true"] {
  border-color: var(--bb-vermillion);
}
.field-error {
  color: var(--bb-vermillion);
  font-family: var(--bb-font-italic);
  font-style: italic;
  font-size: 0.8125rem;
}

/* Orders: the six bastion orders as a compact toggle grid. */
.hb-orders-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 4px 12px;
}
.hb-order-toggle { font-size: 0.9375rem; }

/* Level-outcome tiers: level + text + remove per row. */
.hb-outcome-row {
  display: flex;
  gap: 8px;
  align-items: center;
  margin-bottom: 6px;
}
.hb-outcome-row .hb-outcome-level { flex: 0 0 72px; }
.hb-outcome-row .hb-outcome-text { flex: 1 1 auto; }
.hb-outcome-remove {
  flex: 0 0 auto;
  width: 32px;
  height: 32px;
  background: none;
  border: 1px solid transparent;
  color: var(--bb-ink-soft);
  cursor: pointer;
}
.hb-outcome-remove:hover { color: var(--bb-vermillion); border-color: var(--bb-rule); }
.hb-add-outcome {
  align-self: flex-start;
  background: none;
  border: 1px dashed var(--bb-rule);
  color: var(--bb-ink-soft);
  font: inherit;
  font-size: 0.8125rem;
  padding: 5px 10px;
  cursor: pointer;
}
.hb-add-outcome:hover { border-color: var(--bb-ink-soft); color: var(--bb-ink); }

/* Icon picker: selected disc + a collapsible filterable radiogroup grid.
   Inline (not a popover/nested dialog) — the editor dialog owns the top
   layer, so anything body-appended would render behind it. */
.hb-icon-disclosure { display: flex; align-items: center; gap: 10px; }
.hb-icon-disclosure .glyph { flex: 0 0 auto; }
#hb-icon-toggle {
  background: none;
  border: 1px solid var(--bb-rule);
  color: var(--bb-ink);
  font: inherit;
  font-size: 0.8125rem;
  padding: 6px 10px;
  cursor: pointer;
}
#hb-icon-toggle:hover { border-color: var(--bb-ink-soft); }
#hb-icon-picker { margin-top: 8px; }
#hb-icon-filter { margin-bottom: 8px; }
.hb-icon-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(44px, 1fr));
  gap: 4px;
  max-height: 200px;
  overflow-y: auto;
  padding-right: 12px;
  scrollbar-gutter: stable;
}
.hb-icon-option {
  aspect-ratio: 1;
  min-height: 44px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: var(--bb-parchment-light);
  border: 1px solid var(--bb-rule-soft);
  color: var(--bb-ink);
  cursor: pointer;
}
.hb-icon-option .glyph-svg { width: 24px; height: 24px; }
.hb-icon-option:hover { border-color: var(--bb-ink-soft); }
.hb-icon-option[aria-checked="true"] {
  border-color: var(--bb-vermillion);
  outline: 1px solid var(--bb-vermillion);
  background: rgba(168, 40, 29, 0.08);
}
.hb-icon-option:focus-visible { outline: 2px solid var(--bb-vermillion); outline-offset: -2px; }

/* Width-only by design (#283): stacking a field row makes the dialog TALLER to
   fit a narrow column, and the tier's other branch is a viewport with no height
   to spare. At 828px the rows fit side by side and must stay that way. */
@media (max-width: 700px) {
  .hb-field-row { flex-direction: column; gap: 10px; }
  .hb-orders-grid { grid-template-columns: repeat(2, 1fr); }
}

/* Homebrew management dialog (#146): the library list + New button. */
.hb-new-btn {
  align-self: flex-start;
  margin-bottom: 10px;
  background: none;
  border: 1px dashed var(--bb-rule);
  color: var(--bb-ink);
  font: inherit;
  font-size: 0.875rem;
  padding: 7px 12px;
  cursor: pointer;
}
.hb-new-btn:hover { border-color: var(--bb-ink-soft); }
/* #229: the manager's file verbs share a row with + New. Secondary buttons
   are quiet solid hairlines (dashed stays the add/placeholder affordance);
   resets beat the global button chrome at rest AND :hover (#227 lesson). */
.hb-manage-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  margin-bottom: 10px;
}
.hb-manage-actions .hb-new-btn { margin-bottom: 0; }
.hb-manage-secondary {
  background: none;
  border: 1px solid var(--bb-rule);
  box-shadow: none;
  color: var(--bb-ink-soft);
  font: inherit;
  font-size: 0.8125rem;
  padding: 7px 12px;
  cursor: pointer;
}
.hb-manage-secondary:hover,
.hb-manage-secondary:active {
  background: none;
  border: 1px solid var(--bb-ink-soft);
  box-shadow: none;
  transform: none;
  color: var(--bb-ink);
}
.hb-manage-secondary:focus-visible { outline: 2px solid var(--bb-vermillion); outline-offset: -2px; }
.hb-list {
  list-style: none;
  margin: 0;
  padding: 0 12px 0 0;           /* scroll gutter, house rule */
  max-height: 50vh;
  overflow-y: auto;
  scrollbar-gutter: stable;
}
.hb-list .empty {
  color: var(--bb-ink-soft);
  font-family: var(--bb-font-italic);
  font-style: italic;
  font-size: 0.875rem;
  padding: 8px 2px;
}
.hb-row {
  display: flex;
  /* Three verbs since #227 — on narrow widths the verb cluster wraps to a
     second line instead of crushing the name (.hb-row-body has min-width:0). */
  flex-wrap: wrap;
  align-items: center;
  gap: 10px;
  padding: 8px 10px;
  border: 1px solid var(--bb-rule);
  margin-bottom: 6px;
}
.hb-row-body { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
.hb-row-name {
  font-family: var(--bb-font-display);
  font-size: 1rem;
  color: var(--bb-ink);
}
.hb-row-meta {
  color: var(--bb-ink-soft);
  font-family: var(--bb-font-italic);
  font-style: italic;
  font-size: 0.75rem;
}
.hb-row-verb {
  flex: 0 0 auto;
  background: none;
  border: 1px solid var(--bb-rule);
  color: var(--bb-ink);
  font: inherit;
  font-size: 0.8125rem;
  padding: 5px 10px;
  cursor: pointer;
  min-height: 32px;
}
.hb-row-verb:hover { border-color: var(--bb-ink-soft); }
.hb-row-verb-danger:hover {
  border-color: var(--bb-vermillion);
  color: var(--bb-vermillion-deep);
}
.hb-row-verb:focus-visible { outline: 2px solid var(--bb-vermillion); outline-offset: -2px; }

/* =========================================================================
 * Homebrew editor v2 (#226)
 * ========================================================================= */
/* The three structured-family sections wear .hb-fieldset chrome with a
 * full-width disclosure button standing in for the legend (steward P1 — a
 * real <legend> can't collapse). Row-level errors extend the .field-error
 * convention (steward P5): the offending row/card carries a vermillion
 * left-rule and one message line; aria-invalid still marks the input. */
.hb-disclosure { padding: 0; }
.hb-disclosure > div:not([hidden]) {
  display: flex;
  flex-direction: column;
  gap: 10px;
  padding: 0 12px 12px;
}
.hb-disclosure-toggle {
  display: flex;
  align-items: center;
  gap: 8px;
  width: 100%;
  padding: 10px 12px;
  text-align: left;
  cursor: pointer;
  font-family: var(--bb-font-display);
  font-size: 0.875rem;
  letter-spacing: 0.04em;
  color: var(--bb-ink-soft);
  /* Quiet header, not a parchment button — reset the global button chrome
     at rest AND on :hover/:active (button:hover outspecifies a lone class —
     the #227 lesson). */
  background: none;
  border: none;
  box-shadow: none;
}
.hb-disclosure-toggle:hover,
.hb-disclosure-toggle:active {
  background: none;
  border: none;
  box-shadow: none;
  transform: none;
  color: var(--bb-ink);
}
.hb-disclosure-toggle:focus-visible { outline: 2px solid var(--bb-vermillion); outline-offset: -2px; }
.hb-disclosure-caret {
  display: inline-block;
  transition: transform 0.15s ease;
}
.hb-disclosure-toggle[aria-expanded="true"] .hb-disclosure-caret { transform: rotate(90deg); }
@media (prefers-reduced-motion: reduce) {
  .hb-disclosure-caret { transition: none; }
}
.hb-disclosure-count {
  font-family: var(--bb-font-italic);
  font-style: italic;
  letter-spacing: 0;
  color: var(--bb-ink-faint);
}
.hb-disclosure-hint {
  font-size: 0.8125rem;
  margin: 0;
}
/* Family rows/cards reserve a transparent left rule so flagging an error
   never shifts layout; builders set it, this class only recolors it. */
.hb-row-invalid { border-left-color: var(--bb-vermillion); }

/* Content cards inside a repeatable group (steward P3/P6): SOLID hairline —
   dashed stays reserved for add/placeholder affordances. */
.hb-type-card,
.hb-option-card {
  display: flex;
  flex-direction: column;
  gap: 6px;
  padding: 8px 10px;
  border: 1px solid var(--bb-rule);
  border-left: 3px solid transparent;
  background: var(--bb-parchment);
  margin-bottom: 6px;
}
.hb-family-row-head {
  display: flex;
  align-items: center;
  gap: 8px;
}
.hb-family-row-head > input[type="text"],
.hb-family-row-head > select {
  flex: 1 1 auto;
  min-width: 0;
}
/* Trackers: flat morphing rows (steward P2) — same box as the cards, but
   the kind-specific fields swap inside .hb-tracker-morph. */
.hb-tracker-row {
  display: flex;
  flex-direction: column;
  gap: 8px;
  padding: 8px 10px;
  border: 1px solid var(--bb-rule);
  border-left: 3px solid transparent;
  background: var(--bb-parchment);
  margin-bottom: 6px;
}
.hb-tracker-row .hb-family-row-head select { flex: 0 0 auto; width: auto; }

/* Event-table rows (#256) — the same flat box as a tracker row, because they
   are the same species of thing: one repeatable record with a head line and
   trailing detail. What differs is that the head leads with a ROLL RANGE
   rather than a label, so the range gets a fixed narrow pair and the name
   takes the remaining width. */
.hb-table-row {
  display: flex;
  flex-direction: column;
  gap: 8px;
  padding: 8px 10px;
  border: 1px solid var(--bb-rule);
  border-left: 3px solid transparent;
  background: var(--bb-parchment);
  margin-bottom: 6px;
}
.hb-roll-range {
  display: flex;
  align-items: baseline;
  gap: 4px;
  flex: 0 0 auto;
  font-variant-numeric: lining-nums tabular-nums;
}
/* Wide enough for "100" and no wider. Two separate things have to be undone,
   and only one of them is a specificity fight:
     · `.hb-form input[type="number"] { width: 100% }` needs outranking, hence
       the `.hb-form` + attribute selector;
     · `input, select { min-width: 120px }` (the global field floor) silently
       wins over ANY smaller width regardless of specificity, so `min-width: 0`
       is what actually makes a narrow field narrow here. Without it this rule
       computes to 120px and looks like it never applied. */
.hb-form .hb-roll-range input[type="number"] {
  width: 3.25rem;
  min-width: 0;
  flex: 0 0 auto;
  text-align: center;
  font-variant-numeric: lining-nums tabular-nums;
}
/* The low end is DERIVED from the row above (#256 follow-up), so it's printed,
   not typed — same size and figures as the input beside it so the pair still
   reads as one range rather than a label and a field. */
.hb-roll-lo {
  min-width: 2.5rem;
  text-align: right;
  font-size: 0.9375rem;
  color: var(--bb-ink-soft);
}
.hb-roll-dash { color: var(--bb-ink-faint); }
/* Live coverage, in the disclosure head beside the count. Gilt while the table
   is short of its die, plain once it's covered — the answer to "did I miss
   any numbers?" without saving to find out. */
.hb-rows-coverage { font-variant-numeric: lining-nums tabular-nums; }
.hb-rows-coverage.is-short { color: var(--bb-gilt-deep); }

/* THE CALENDAR'S MONTHS (#310) — a ruled table, not the event-table row
   anatomy. Rendered with real data, that anatomy put 18 Harptos rows at 1745px
   of scroll and repeated the format hint eighteen times; an event table has
   6-10 rich rows, a calendar row has three fields. This is 819px for the same
   18 — one screen, no scroll — which is what a rename pass over a duplicated
   calendar actually needs. */
.hb-cal-row {
  display: grid;
  grid-template-columns: 2.2rem minmax(0, 13rem) 3.4rem 2.6rem 3.4rem 2rem;
  align-items: center;
  gap: 0.4rem;
  border-bottom: 1px dotted var(--bb-rule-soft);
  padding: 0;
}
/* Headings once, at the top, in the small-caps label voice this dialog already
   uses. Without them the days and leaps columns are a bare number and an empty
   box, and the reader has to infer both from the hint paragraph. */
.hb-cal-head {
  border-bottom: 1px solid var(--bb-rule-soft);
  padding-bottom: 3px;
  margin-bottom: 2px;
  font-family: var(--bb-font-display);
  font-size: 0.66rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--bb-ink-faint);
}
.hb-cal-head > span:nth-child(3),
.hb-cal-head > span:nth-child(5) { text-align: right; }
.hb-cal-head > span:nth-child(4) { text-align: center; }
/* The running day-of-year: a readout, so it takes the faint ink of a label
   rather than the settled ink of something you typed. */
.hb-cal-doy {
  font-family: var(--bb-font-italic);
  font-style: italic;
  font-size: 0.75rem;
  color: var(--bb-ink-faint);
  text-align: right;
  font-variant-numeric: lining-nums tabular-nums;
}
/* Borderless on a dotted rule, so the table reads as a ledger page. The rule is
   PERMANENT rather than hover-revealed: on touch there is no hover, and a
   borderless field with nothing under it reads as read-only. */
.hb-cal-row input[type="text"],
.hb-cal-row input[type="number"] {
  min-width: 0;
  width: 100%;
  max-width: none;
  background: transparent;
  border: none;
  border-bottom: 1px solid transparent;
  border-radius: 0;
  /* The global `input, select` rule draws a SUNKEN SHADOW, and leaving it in
     place is what made every cell read as a filled box on a ruled page: the
     borders were gone but the shadow was still drawing them. */
  box-shadow: none;
  padding: 2px;
}
.hb-cal-row input:focus {
  outline: none;
  border-bottom-color: var(--bb-ink-soft);
  box-shadow: none;
}
.hb-cal-row .hb-cal-fest { display: flex; justify-content: center; cursor: pointer; }
.hb-cal-days, .hb-cal-leap { text-align: right; font-variant-numeric: lining-nums tabular-nums; }
/* Touch targets are a COARSE-POINTER concern, and stating a 44px floor
   unconditionally is what turned a 16-row calendar into 1184px of scrolling on
   a desktop where nothing needs it. The ruled table's whole argument is that it
   fits on one screen; a floor that applies to mice destroys that. */
@media (pointer: coarse) {
  .hb-cal-row input[type="text"],
  .hb-cal-row input[type="number"],
  .hb-cal-row .hb-cal-fest { min-height: 44px; }
}
/* A leap-only period falls outside the base year, so it is set back a little to
   say so without a second column of explanation. */
.hb-cal-row.is-leap-only .hb-cal-name { color: var(--bb-ink-soft); font-style: italic; }
.hb-months-tally { font-variant-numeric: lining-nums tabular-nums; }
.hb-months-tally.is-short { color: var(--bb-gilt-deep); }
.hb-row-hint {
  width: 100%;
  font-family: var(--bb-font-italic);
  font-style: italic;
  font-size: 0.9375rem;
}
.hb-row-condition {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: 8px 14px;
}
/* The kind select leads and sizes to its own text; its two trailing fields
   share what's left, so "No lingering effect" (the common case) sits alone on
   the line rather than beside two empty boxes. */
.hb-row-condition > select { flex: 0 1 auto; min-width: 0; }

/* Casualties share the condition row's shape — both are "this row also does
   something mechanical", folded away until asked for. */
.hb-row-casualty {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: 8px 14px;
}
.hb-row-casualty > .view-toggle { flex: 0 0 auto; }
/* The INPUT has to be sized too, not just its wrapper. Number inputs here carry
   a fixed 120px from the tracker morph's sizing, so narrowing only the
   .hb-mini-field left the input overflowing its own field by 40px and running
   under the note beside it. */
.hb-row-casualty .hb-mini-field { flex: 0 0 4.5rem; }
.hb-row-casualty .hb-mini-field input { width: 100%; min-width: 0; }
/* The rule the checkbox imports, spelled out — this field is the one place a
   homebrew row inherits mechanics rather than a value, so the row says which. */
.hb-casualty-note { flex: 1 1 16rem; min-width: 0; font-size: 0.8rem; }

.hb-tracker-morph {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: 8px 14px;
}
.hb-mini-field {
  display: flex;
  flex-direction: column;
  gap: 3px;
  flex: 1 1 120px;
  min-width: 0;
}
.hb-mini-label {
  font-family: var(--bb-font-display);
  font-size: 0.75rem;
  color: var(--bb-ink-soft);
}
.hb-mini-check {
  flex: 0 0 auto;
  flex-direction: row;
  align-items: center;
  gap: 6px;
  font-size: 0.8125rem;
  color: var(--bb-ink);
  padding-bottom: 7px; /* baseline-align with neighboring inputs */
}
.hb-mini-check input[type="checkbox"] { width: auto; }
.hb-state-pair {
  display: flex;
  gap: 8px;
}
.hb-state-pair input { min-width: 0; }
/* Loadout expands into an indented block under the row (steward P2). */
.hb-loadout-block {
  flex: 1 1 100%;
  display: flex;
  flex-direction: column;
  gap: 8px;
  margin-left: 10px;
  padding-left: 10px;
  border-left: 1px solid var(--bb-rule-soft);
}
.hb-loadout-option {
  display: flex;
  gap: 8px;
  align-items: center;
}
.hb-loadout-option input { min-width: 0; }
.hb-loadout-option input[data-opt="label"],
.hb-loadout-option input[data-field$=".label"] { flex: 3 1 0; }
.hb-loadout-option input[data-opt="hint"],
.hb-loadout-option input[data-field$=".hint"] { flex: 2 1 0; }
.hb-loadout-pick {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: 8px 14px;
}
.hb-pick-pair {
  display: flex;
  gap: 8px 14px;
  flex: 1 1 auto;
}

/* Order options: card internals (steward P3). Effect blocks are the
   omit-don't-disable zone — ghost +Add buttons materialize them. */
.hb-option-card .hb-family-row-head select {
  flex: 0 0 auto;
  width: auto;
}
.hb-option-meta {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: 8px 14px;
}
.hb-effect-blocks {
  display: flex;
  flex-direction: column;
  gap: 8px;
}
.hb-effect-block {
  display: flex;
  flex-direction: column;
  gap: 6px;
  padding: 6px 8px;
  border: 1px solid var(--bb-rule-soft);
  background: var(--bb-parchment-mid);
}
.hb-effect-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
}
.hb-effect-fields {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: 8px 14px;
}
.hb-effect-rows {
  display: flex;
  flex-direction: column;
  gap: 6px;
}
.hb-effect-line {
  display: flex;
  gap: 8px;
  align-items: center;
}
.hb-effect-line input,
.hb-effect-line select {
  min-width: 0;
  flex: 1 1 auto;
}
.hb-effect-line input[type="number"] { flex: 0 1 80px; }
.hb-effect-line .hb-write-value { flex: 0 1 120px; display: flex; }
.hb-effect-line .hb-write-value > * { width: 100%; }
.hb-ghost-row {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
}
.hb-ghost-row .hb-ghost-add { align-self: auto; }

/* New-in-#226 removes must clear the 44px thumb bar on touch; bumping the
   legacy 32px outcome-row ✕ along with them is a welcome side effect. */
@media (pointer: coarse) {
  .hb-outcome-remove {
    width: 44px;
    height: 44px;
  }
}

/* ≤700px: family morph zones and effect lines stack; cards stay cards
   (steward P7). Width-only, same reasoning as .hb-field-row above (#283). */
@media (max-width: 700px) {
  .hb-tracker-morph,
  .hb-option-meta,
  .hb-effect-fields,
  .hb-loadout-pick {
    flex-direction: column;
    align-items: stretch;
  }
  .hb-effect-line,
  .hb-loadout-option {
    flex-wrap: wrap;
  }
  .hb-mini-field { flex: 1 1 auto; }
  .hb-state-pair { flex-direction: column; }
  .hb-pick-pair { flex-direction: column; }
}

/* Picker footer "Manage homebrew…" (#146 steward ruling b): the management
   door where homebrew is experienced. Quiet link idiom, pushed opposite the
   Back button; never competes with commissioning. */
.picker-manage-link {
  margin-left: auto;
  /* The bare .picker-hint-link inherits its font from a styled parent (the
     prereq hint sets the italic face); this one sits in the plain footer, so
     it must speak the footer's voice itself — same recipe as .picker-back. */
  font-family: var(--bb-font-italic);
  font-style: italic;
  font-size: 0.8125rem;
}
@media (max-width: 700px), (max-height: 500px) and (pointer: coarse) {  /* tier — tap target (#283) */
  .picker-manage-link { min-height: 44px; }
}

/* =========================================================================
 * HOME — the front door (#264)
 *
 * web/home.html. The page is a REGISTER, not a pitch: a masthead, then one
 * ruled column of variable-height entries. Variable row height is the point —
 * it is the only anatomy where "names never truncate" is structurally free
 * rather than a special case, and the only one that doesn't render a single
 * lonely card beside two dead columns at the median library size of one.
 *
 * Every colour is a --bb-* token, so all four themes are covered without a
 * single [data-theme] rule below. Net addition to @layer themes: zero.
 * ========================================================================= */

/* Home's own bar: the wordmark is suppressed at EVERY width, not just on
 * phones. The h1 immediately below carries the name in full with the
 * illuminated initial, and a page that says "Bastion Builder" twice ~40px
 * apart makes the drop cap read as decoration instead of identity. */
.home-page-bar .wordmark { display: none; }

/* The bar's two controls are ANCHORS, not buttons — they navigate, and a page
 * that must work with JS off cannot fake that with a click handler. The base
 * button recipe is element-scoped (`button`, `button.btn-primary`), so it does
 * not reach them; these rules re-apply it rather than restating a new one, and
 * the two must stay in step with the `button` block above.
 *
 * Gilt on the return control never collides with the gilt "Create your
 * Bastion" tile: the tile exists only in the empty population, where this
 * control is the plain "Open the app". They are never on screen together. */
/* Add ONLY what an anchor lacks. Deliberately no font-family and no colour
 * here: `.top-bar .btn-ghost` (styles.css:2581) already sets the display face,
 * size, tracking and ink for this bar, and an earlier version of this block
 * overrode it with the body face — which rendered the bar's labels as
 * uppercase Cormorant, visibly a different font from every other page.
 * Anything this block declares must be a property the shared rule doesn't. */
.home-page-bar a.btn-ghost,
.home-page-bar a.btn-primary {
  /* inline-block, NOT inline-flex: flex layout drops whitespace-only text
     nodes between children, which ate the space before the label's arrow. */
  display: inline-block;
  text-decoration: none;
  white-space: nowrap;
}
/* btn-primary is the one that genuinely has no anchor-reachable recipe —
 * `button.btn-primary` is element-scoped — so it restates the gilt fill. Keep
 * in step with the `button.btn-primary` block. */
.home-page-bar a.btn-primary {
  padding: 6px 12px;
  border-radius: 0;
  background: linear-gradient(180deg, var(--bb-gilt-bright), var(--bb-gilt));
  color: var(--bb-ink);
  border: 1px solid var(--bb-gilt-deep);
  font-family: var(--bb-font-display);
  font-size: 0.84rem;
  letter-spacing: 0.10em;
  text-transform: uppercase;
  box-shadow:
    0 1px 0 var(--bb-inset-strong) inset,
    0 2px 4px var(--bb-shadow);
}
.home-page-bar a.btn-primary:hover {
  background: linear-gradient(180deg,
    color-mix(in oklch, var(--bb-gilt-bright), white 6%),
    var(--bb-gilt-bright));
}
.home-page-bar a:focus-visible {
  outline: 2px solid var(--bb-vermillion);
  outline-offset: 2px;
}

/* 820px, not the 960 the register alone would like: the masthead's prose sets a
 * readable measure (~62ch here), and a column wide enough for the roll made the
 * text stop less than two-thirds across, so the page read as two different
 * widths stacked. One column, one left edge, one right edge. */
.home-page {
  max-width: 820px;
  margin-inline: auto;
  padding: 18px 24px 40px;   /* the .help-page rhythm — home and the guide are siblings */
}

/* ---- Masthead ----------------------------------------------------------- */

/* Composed, not stacked: the drop cap, the standfirst and the safety line lock
 * to one left margin, so the vermillion initial has structure under it. */
.home-masthead {
  margin: 6px 0 26px;
}
.home-title {
  margin: 0 0 6px;
  font-family: var(--bb-font-display);
  font-size: clamp(1.9rem, 4vw, 2.4rem);
  line-height: 1.15;
  font-weight: 600;
  letter-spacing: 0.02em;
  color: var(--bb-ink);
}
/* The app's own unconditional gesture, borrowed once. The 1.75em ratio is the
 * Charter's (56px cap on a 2rem name) — not an invented 3.2em. This is the
 * THIRD sanctioned ::first-letter site in the system; it is deliberately not
 * repeated down the register, where twelve identical capitals would stop being
 * illumination and become a bullet. */
.home-title::first-letter {
  float: left;
  font-family: var(--bb-font-display);
  font-size: 1.75em;
  line-height: 0.72;
  margin: 0 6px 0 0;
  color: var(--bb-vermillion);
  text-transform: uppercase;
}
/* No ch cap: the column itself is the measure now (see .home-page). */
.home-standfirst {
  margin: 0 0 14px;
  font-family: var(--bb-font-text);
  font-size: 1.15rem;
  line-height: 1.5;
  color: var(--bb-ink-soft);
}
/* Full width, unlike the text above and below it. A rule that stopped at the
 * measure made the masthead read as an unfinished column beside dead space;
 * spanning the page ties the left-aligned frontispiece to the roll below and
 * makes the asymmetry read as chosen. */
.home-rule-dotted {
  border-top: 1px dotted var(--bb-rule-soft);
  margin: 0 0 14px;
}
/* ROMAN, at full ink. This is the claim, not a gloss on it — and it is the
 * single most load-bearing sentence on the page for an OAuth reviewer. Being
 * on-theme is not a licence to set it in small italic. */
.home-safety {
  margin: 0;
  font-family: var(--bb-font-text);
  font-size: 1rem;
  line-height: 1.5;
  color: var(--bb-ink);
}
.home-safety strong { font-weight: 600; color: var(--bb-ink); }

/* ---- Bays --------------------------------------------------------------- */

.home-bay { margin-bottom: 30px; }

/* .section-head is a two-way space-between, so the filter and the count share
 * one right-hand aside rather than becoming a third child floating mid-header. */
/* The gap matters: the filter carries a bottom border, and with the head flush
 * against the roll the two hairlines stacked and read as one double-weight
 * rule. 7px separates them into what they are. */
.home-bay .section-head { margin-bottom: 7px; }
.home-head-aside {
  display: flex;
  align-items: baseline;
  gap: 12px;
  min-width: 0;
}
.head-count {
  font-family: var(--bb-font-italic);
  font-style: italic;
  font-size: 0.85rem;
  color: var(--bb-ink-faint);
}
.home-filter {
  min-width: 0;
  max-width: 180px;
  padding: 2px 4px;
  font-family: var(--bb-font-text);
  font-size: 0.9rem;
  color: var(--bb-ink);
  background: transparent;
  border: 0;
  border-bottom: 1px solid var(--bb-rule-soft);
}
.home-filter:focus-visible {
  outline: 2px solid var(--bb-vermillion);
  outline-offset: 2px;
}
/* The arrange control (#267) — states how the register is currently sorted and
 * opens the alternatives. Deliberately NOT .home-foot-btn: that is 0.72rem
 * tracked uppercase, and dropping it into a three-item aside beside a 0.9rem
 * input and a 0.85rem italic makes three typographic registers in one 24px
 * band. One step darker than .head-count, because the count is marginalia and
 * this is a control. */
.home-arrange {
  flex: 0 0 auto;
  padding: 0;
  font-family: var(--bb-font-italic);
  font-style: italic;
  font-size: 0.85rem;
  color: var(--bb-ink-soft);
  background: transparent;
  border: 0;
  box-shadow: none;
  cursor: pointer;
}
.home-arrange:hover { color: var(--bb-vermillion); }
.home-arrange:focus-visible {
  outline: 2px solid var(--bb-vermillion);
  outline-offset: 2px;
}

/* ---- The roll ----------------------------------------------------------- */

/* The .reg-card surface recipe, so the roll is recognisably a
 * Register-of-Chambers cousin rather than a new kind of panel. */
.home-roll {
  min-width: 0;
  background: var(--bb-parchment-light);
  border: 1px solid var(--bb-rule);
  box-shadow: var(--shadow-card);
}
/* A big library is the success case, not the edge case — 17 bastions ran the
 * page metres long and buried the key band and colophon below it. The roll
 * scrolls internally past roughly six entries so the page itself stays a fixed,
 * scannable height; the filter (rendered above the same threshold) is the other
 * half of that answer. One column is kept deliberately: variable row height and
 * never-truncated names are the register's whole anatomy, and a two-up grid of
 * smaller cards gives both of those up. */
.home-roll-body {
  min-width: 0;
  max-height: min(62vh, 680px);
  overflow-y: auto;
  scrollbar-gutter: stable;
  padding-right: 12px;
}
/* No cap when there is nothing to scroll — the three first-run choices must not
 * sit in a scroll box. */
.home-roll-body.is-blank {
  max-height: none;
  overflow-y: visible;
  scrollbar-gutter: auto;
}

/* The empty population sits in the exact slot the entries will occupy: same
 * padding, same dotted separator. That makes "this is where your entries go"
 * true at the pixel level instead of by assertion — no invented ruled-ledger
 * gradient, which could never align with the tiles anyway. */
.home-roll-body.is-blank { padding: 14px 16px; }
.home-roll-body.is-blank .dialog-actions-list { gap: 0; margin: 0; }
.home-roll-body.is-blank .dialog-choice { text-decoration: none; }
.home-roll-body.is-blank .dialog-choice + .dialog-choice {
  margin-top: 14px;
  padding-top: 14px;
  border-top: 1px dotted var(--bb-rule-soft);
}

/* Row-gap 10, not 8: at 8px two lines of 0.72rem tracked caps read as a wrapped
 * line, at 10px they read as two rows. No effect when the rail fits on one. */
.home-roll-foot {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 10px 18px;
  padding: 12px 16px;
  border-top: 1px solid var(--bb-rule);
}
/* The verbs sit in two runs (see home.js renderFoot). Same display, same gap,
 * same baseline as the rail itself, so on one line the runs are invisible. */
.home-foot-run {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 10px 18px;
}
/* Two poles, not two ragged lines. The library run takes the right edge, so a
 * rail that folds folds into a composed split — start-something at the left
 * margin, library custody at the right — instead of a long line hanging off a
 * short one. Whether it folds at all depends on the theme (Cinzel is far wider
 * than IM Fell at the same size), which is exactly why the fit is not allowed
 * to be load-bearing: both outcomes are the same composition. The same gesture
 * as .home-colophon-links .version-badge, eleven rules below — the page's foot
 * already speaks left-pole / right-pole. Not in the empty population, where the
 * lead-in question must stay beside its answers. */
.home-roll-foot:not(.is-blank) .home-foot-run + .home-foot-run {
  margin-left: auto;
}
.home-foot-lead {
  font-family: var(--bb-font-italic);
  font-style: italic;
  font-size: 0.85rem;
  color: var(--bb-ink-soft);
}
/* The rail mixes anchors (navigate) and one <button> (Import opens a file
 * picker, which needs a real click). They must be indistinguishable, so the
 * button's inherited chrome — gradient, border, shadow, padding — is stripped
 * back to the anchors' plain text verb. */
.home-foot-btn {
  padding: 0;
  background: none;
  border: 0;
  box-shadow: none;
  font-family: var(--bb-font-display);
  font-size: 0.72rem;
  font-weight: 400;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--bb-ink-soft);
  text-decoration: none;
  cursor: pointer;
}
.home-foot-btn:hover { background: none; border: 0; }
.home-foot-btn:hover { color: var(--bb-vermillion); }
.home-foot-btn:focus-visible {
  outline: 2px solid var(--bb-vermillion);
  outline-offset: 2px;
}

/* ---- One entry ---------------------------------------------------------- */

.holding {
  position: relative;
  display: grid;
  grid-template-columns: 84px 1fr auto;
  gap: 0 14px;
  align-items: start;
  padding: 14px 16px;
}
.holding + .holding { border-top: 1px dotted var(--bb-rule-soft); }
/* Hover ground only. The name does NOT go vermillion-deep on hover: in the
 * saves vocabulary that colour means CURRENT (.bastions-menu-row.current
 * .bm-load), and painting every hovered row with it makes each one impersonate
 * the current bastion. */
.holding:hover,
.holding:focus-within {
  background: var(--bb-gilt-fill);
  transition: background-color 90ms ease;
}
.holding.is-current .hd-name { color: var(--bb-vermillion-deep); }

/* One mechanism at every width: an absolute overlay, with the verbs lifted
 * above it. Survives the ≤640px switch to a float layout, which a grid-area
 * span would not. */
.hd-open {
  position: absolute;
  inset: 0;
  padding: 0;
  background: none;
  border: 0;
  box-shadow: none;
  cursor: pointer;
}
/* The global button chrome has to be beaten PER STATE — `button:hover` is
 * (0,1,1) and outspecifies a lone class, the same lesson .picker-hint-link and
 * the #227 header link each record above. It matters more here than anywhere
 * else in the app: this button is an absolutely-positioned overlay covering the
 * whole row, and absolute positioning paints it above every in-flow sibling
 * regardless of DOM order. So the inherited `:hover` gradient — an OPAQUE
 * parchment-light fill — painted straight over the card. Hovering a row blanked
 * its name, figures, standing and filing line; the only survivors were the
 * elements that happen to carry `position: relative` and are therefore lifted
 * into the same layer (the seal, and any `[data-tip]` in the body — which is
 * why a party bastion kept exactly its "owned by …" line and nothing else).
 * The row's own hover ground lives on `.holding:hover` and is unaffected. */
.hd-open:hover,
.hd-open:active {
  background: none;
  border: 0;
  box-shadow: none;
  transform: none;
}
.hd-open:focus-visible {
  outline: 2px solid var(--bb-vermillion);
  outline-offset: -2px;
}

/* Tipped text in the body column has to sit ABOVE that overlay, or its tooltip
 * is dead on arrival: tooltip.js resolves with `ev.target.closest("[data-tip]")`,
 * and .hd-open covers the whole row while carrying no tip of its own — so the
 * pointer never lands on anything tipped and closest() returns null.
 *
 * Three separate features had each shipped a tip into this card body before
 * anyone noticed (the wax seal's "read-only", the multi-owner roster, and #265's
 * exact snapshot date), which is why this is a rule about the column rather than
 * a fix to one of them. Positioned and later in DOM order beats the earlier
 * absolutely-positioned sibling, with no z-index needed.
 *
 * The trade is that these elements now swallow the click the overlay would have
 * taken, so each card hands it back — see buildEntry in home.js / home-shelf.js. */
.hd-body [data-tip] { position: relative; }

.hd-seal {
  position: relative;
  /* Centred, not start-aligned. The row's height follows whichever is taller,
     so with a short body (no owner line) the seal filled the row and read as
     centred, while a taller body left it hanging at the top — the same seal
     looking like two different decisions down one column. Centring is the only
     rule that looks the same in both. */
  align-self: center;
  width: 84px;
  height: 84px;
  background: var(--bb-parchment);
  border: 1px solid var(--bb-rule);
  box-shadow: var(--shadow-sunken);
}
.hd-seal svg { display: block; width: 100%; height: 100%; }
.hd-seal.is-empty {
  display: flex;
  align-items: center;
  justify-content: center;
  outline: 1px dashed var(--bb-rule-soft);
  outline-offset: -5px;
}
.hd-seal-empty {
  font-family: var(--bb-font-italic);
  font-style: italic;
  font-size: 0.65rem;
  text-align: center;
  color: var(--bb-ink-soft);
}

/* The seal's marks (#302). ONE ink — --bb-wall — separated by VALUE, which is
 * what a plan actually encodes: paper, paving, chamber floor, masonry. The
 * special/basic hue split was retired here and the reasons are worth keeping,
 * because they are not preference: at seal scale it answered a question no
 * reader asks, it was undecodable without a legend, and the two tokens SWAP
 * ROLES between themes — in cartographer --bb-vermillion is deep blue and
 * --bb-sapphire is brown, so "red = special" was already false in a quarter of
 * the app. --bb-wall is the only token meaning structural ink in all four, and
 * it inverts to luminous in arcane for free.
 *
 * The payoff is at the Register's leaf: .fp-lit is now the ONLY chromatic mark
 * in the drawing. Measured composited over the real host grounds, the shipped
 * seal separated the inked chamber from its 26%-tinted siblings by 2.38-2.78:1
 * — under the 3:1 non-text bar in all four themes. Against the floor it now
 * clears 3:1 in all four (worst 3.34, arcane).
 *
 * .fp-floor, .fp-paving and .fp-deck mix over a TOKEN, not `transparent`. That
 * opacity is what stops the ruling at the plan's edge, and it is the whole
 * figure/ground mechanism — do not "simplify" them into alpha washes.
 *
 * The `fp-` prefix is load-bearing — see the header note in home-footprint.js
 * before renaming any of these. */
.fp-rule { fill: none; stroke: color-mix(in srgb, var(--bb-wall) 13%, transparent); }
.fp-rule-5 { fill: none; stroke: color-mix(in srgb, var(--bb-wall) 22%, transparent); }
.fp-floor { fill: var(--bb-parchment-light); }
.fp-paving { fill: color-mix(in srgb, var(--bb-wall) 10%, var(--bb-parchment-light)); }
.fp-masonry { fill: color-mix(in srgb, var(--bb-wall) 74%, transparent); }
/* The massing, below ~1.6px per cell: one tone standing in for an interior
 * there is no room to draw. Between paving and masonry deliberately — it is
 * neither, it is a building seen too small to read. */
.fp-silhouette { fill: color-mix(in srgb, var(--bb-wall) 30%, var(--bb-parchment-light)); }
/* The derived boundary and the party walls between abutting chambers. The
 * contour is a HAIRLINE where real masonry is a filled band, so the seal never
 * claims wall thickness the bastion does not have. */
.fp-contour { fill: none; stroke: color-mix(in srgb, var(--bb-wall) 88%, transparent); stroke-linecap: square; }
.fp-party { fill: none; stroke: color-mix(in srgb, var(--bb-wall) 46%, transparent); }
/* The Register's leaf inks the chamber it is reading (#300) — the one colour on
   the sheet, and drawn at every tier because its absence is a functional
   failure rather than a legibility trade. */
.fp-lit { fill: color-mix(in srgb, var(--bb-vermillion) 72%, transparent); }
/* Vehicle hull (#264): a mobile bastion is a few loose rectangles without it,
 * and unrecognisable as a ship. There is no masonry on a ship, so the deck
 * plate stands in for the silhouette and the hull carries the edge. */
.fp-deck { fill: color-mix(in srgb, var(--bb-wall) 20%, var(--bb-parchment-light)); }
.fp-hull {
  fill: none;
  stroke: color-mix(in srgb, var(--bb-wall) 82%, transparent);
}

.hd-body {
  display: flex;
  flex-direction: column;
  gap: 3px;
  min-width: 0;
}
.hd-name {
  font-family: var(--bb-font-display);
  font-size: 1.3rem;
  font-weight: 500;
  letter-spacing: 0.01em;
  color: var(--bb-ink);
  /* Never truncated, never clamped — the whole reason this is a register. */
  overflow-wrap: anywhere;
}
.hd-figures {
  font-family: var(--bb-font-display);
  font-size: 0.8rem;
  letter-spacing: 0.06em;
  color: var(--bb-ink-soft);
  font-variant-numeric: lining-nums tabular-nums;
}
/* Over cap is an alert, not brand structure — the danger token, as in
 * .cap-echo-fill.over. The numerals already say it; the colour is redundant so
 * the row survives a monochrome print. */
.hd-over { color: var(--bb-danger); font-weight: 600; }
.hd-standing {
  font-family: var(--bb-font-italic);
  font-style: italic;
  font-size: 0.82rem;
  color: var(--bb-ink-faint);
  font-variant-numeric: tabular-nums;
}
/* "Filed under Curse of Strahd" (#267) — last in the body and one step quieter
 * than .hd-standing. The body reads name → who holds it → what's in it → where
 * the record stands → how you filed it, and filing goes last because it is the
 * only line on the card that isn't about the bastion.
 *
 * Not a chip beside the name: .hd-name already carries .bm-dot or .hd-wax, and
 * names never truncate, so they wrap into where a chip would sit. */
.hd-filed {
  font-family: var(--bb-font-italic);
  font-style: italic;
  font-size: 0.8rem;
  color: var(--bb-ink-faint);
}
/* Grouped, the sticky head above the row already says this — the same
 * no-inline-repeats rule that keeps a card from echoing its own heading. It is
 * kept when grouping is OFF for two reasons: below the organise threshold there
 * is nothing to section, so this line is the only visible result of filing at
 * all; and it shows *why* a row matched when a campaign name is typed into the
 * filter. */
.home-roll-body.is-grouped .hd-filed { display: none; }
/* The reader's private note (#277) — last in the body, after .hd-filed. The two
 * form the not-about-the-bastion tail: filing is structural (it says where the
 * row sits in the register), the note is content, so the note goes last.
 *
 * A left hairline rather than a third ink-faint italic line. .hd-standing and
 * .hd-filed are already that, and a fourth is mush — so this steps OUT of the
 * descending gradient instead of extending it: the marginal-rule idiom says
 * "an insertion, not a card field", and once the rule-edge carries that, the
 * ink can go back up to --bb-ink-soft. It is the most actionable line on the
 * row despite sitting last. No pencil glyph: it would imply the row edits in
 * place, which is exactly what .hd-verbs refuses.
 *
 * One clipped line, never expanding. It is a reminder of what you wrote, not
 * the note — the dialog is the note.
 *
 * Two rules that deliberately do NOT follow .hd-filed above:
 *  - no .is-grouped hide. A campaign head repeats a filing; nothing repeats a
 *    note.
 *  - no [data-tip]. That keeps it clear of the .hd-open overlay entirely (no
 *    tip → no position:relative lift → no click hand-back to wire), and a
 *    hover-revealed panel of private prose appearing unbidden would be the
 *    wrong answer regardless. */
.hd-note {
  border-left: 1px solid var(--bb-rule-soft);
  padding-left: 8px;
  font-family: var(--bb-font-italic);
  font-style: italic;
  font-size: 0.8rem;
  color: var(--bb-ink-soft);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* Campaign sections (#267) — the .furnish-group-label running-head, second
 * instance, so this is now a pattern rather than a one-off. Deliberately NOT a
 * .section-head alike: that is the bay-level device (h2 + aside), and repeating
 * it inside a roll makes every campaign look like its own bay.
 *
 * Sticky because the count is the point — it tells a reader whether scrolling
 * further stays inside this campaign. --bb-parchment-light is the roll's own
 * surface (.home-roll above); --bb-parchment here would read as a patch. The
 * negative right margin reaches past .home-roll-body's 12px scrollbar gutter so
 * rows can't ghost through under the head. */
.holding-group-label {
  position: sticky;
  top: 0;
  z-index: 1;
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 10px;
  margin-right: -12px;
  padding: 5px 16px;
  background: var(--bb-parchment-light);
  border-bottom: 1px solid var(--bb-rule-soft);
  font-family: var(--bb-font-display);
  font-size: 0.7rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--bb-ink-faint);
}
/* A campaign name is a user string and wraps rather than truncating, per the
 * never-truncate rule; the count holds its own line-end. */
.hg-name { min-width: 0; }
.hg-count {
  flex: 0 0 auto;
  font-family: var(--bb-font-italic);
  font-style: italic;
  letter-spacing: 0;
  text-transform: none;
  font-variant-numeric: tabular-nums;
}
/* The first head sits flush against the roll's top edge; every later one gets
 * air from the section it opens. */
.holding-group + .holding-group .holding-group-label { margin-top: 6px; }

/* Filter matched nothing (#267) — a gap the library had and the shelf would
 * have inherited with its new filter. Left-aligned: the roll is a ledger, not a
 * card. No echo of the query (it would need escaping and can run sixty
 * characters) and no Clear verb — the search input has a native ×, and the
 * count beside it already reads "0 of 9 bastions". */
.home-roll-empty {
  margin: 0;
  padding: 18px 16px;
  font-family: var(--bb-font-italic);
  font-style: italic;
  font-size: 0.9rem;
  color: var(--bb-ink-faint);
}

/* ---- The "File under a campaign" dialog (#267) -------------------------- */

/* The campaigns already in use, as tappable chips.
 *
 * NOT decoration for the <datalist> beside it — the two do different jobs. A
 * datalist gives keyboard typeahead on a desktop and close to nothing on a
 * phone, so without these the whole point of the picker ("the second bastion in
 * a campaign is a pick, not a retype") is false on touch. Both ship. */
.filing-suggest {
  display: flex;
  flex-wrap: wrap;
  gap: 8px 16px;
  margin-top: 10px;
}
/* .home-foot-btn carries the verb recipe and the coarse-pointer 44px floor
 * already; this only holds them off the flex stretch. */
.filing-chip { flex: 0 0 auto; }
/* Says where the filing is written. On a library row that is reassurance; on a
 * SHELF row it is the sentence that makes filing legible as filing rather than
 * as editing somebody else's keep, which is why it is one string for both.
 * Deliberately not .dialog-warn — that is gilt, and gilt means caution. This is
 * a fact. Same recipe as .annotation-legend-hint. */
.filing-note {
  margin: 12px 0 0;
  font-family: var(--bb-font-italic);
  font-style: italic;
  font-size: 0.8rem;
  color: var(--bb-ink-soft);
}

/* Always visible, never hover-revealed: home is the authoritative management
 * surface, and a verb row that appears only under a pointer is unreachable by
 * touch and unscannable by keyboard. */
.hd-verbs {
  position: relative;
  z-index: 1;
  display: flex;
  gap: 2px;
}

/* ---- The DM's Shelf (#255) ---------------------------------------------- */

/* The second register: bastions that are not yours. Same anatomy as the
 * library's rows on purpose — both answer "which bastion do I open?" — so the
 * only thing this modifier changes is what says CUSTODY. */

/* The wax seal, in the slot .bm-dot occupies on a library row. Same slot and
 * the same ink, but it must not be the same MARK: rendered as a disc it was
 * pixel-for-pixel the library's current-dot, and with both registers on one
 * page that reads as "this is the open bastion" twice. A ring reads as an
 * impression rather than a state light — a seal pressed into wax, which is
 * exactly the claim ("someone else's, stamped and closed"). Deliberately
 * typographic and not an emoji: it would be the one full-colour object on a
 * page whose entire mark vocabulary is set in type. */
.holding .hd-wax {
  display: inline-block;
  /* NOT .bm-dot's fixed 0.8em box: a ring is a wider glyph than a disc at the
     same size, so it overflowed the box and sat flush against the name. Sized
     by its own advance, with the gap as a real margin. */
  margin-right: 0.28em;
  color: var(--bb-vermillion-deep);
  /* Larger than .bm-dot's 0.8em because a ring carries less ink than a disc of
     the same size and would otherwise sit lighter than the mark it replaces. */
  font-size: 0.74em;
  /* Optical centring against a 1.3rem display face — the glyph is small enough
     that baseline alignment drops it below the name's visual midline. */
  vertical-align: 0.16em;
  flex: 0 0 auto;
}
/* A shelf row is somebody else's, and the whole bay is read-only, so the name
 * stays plain ink — .is-current's vermillion-deep would claim this is the open
 * bastion, which it can never be. */
.holding.is-shared .hd-name { color: var(--bb-ink); }

/* A snapshot whose blob no longer decodes. It keeps its row (losing one
 * silently is worse) but drops to marginalia weight, since the only useful
 * verb left is Remove. */
.holding.is-damaged .hd-name { color: var(--bb-ink-faint); }
.holding.is-damaged .hd-standing { color: var(--bb-danger); font-style: normal; }
.holding .bm-remove:hover { color: var(--bb-danger); background: transparent; }

/* The bay's own lead-in. Lighter than a .home-standfirst — this explains a
 * register, it doesn't introduce the page. */
.home-shelf-sub {
  margin: 0 0 10px;
  font-family: var(--bb-font-italic);
  font-style: italic;
  font-size: 0.86rem;
  color: var(--bb-ink-faint);
}

/* Paste rail — the shelf's foot. Reuses .home-roll-foot's frame, so it sits
 * exactly where the library's verb rail sits, and .home-filter's field, so the
 * page has one input treatment rather than two. */
.shelf-rail {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  width: 100%;
}
/* Grows into the rail, unlike the fixed-width filter it borrows its look from:
 * what goes in here is a 20KB link, and a field sized for "Filter…" would show
 * the user about eight characters of it. */
.shelf-rail .shelf-paste {
  flex: 1 1 16rem;
  min-width: 0;
  font-family: var(--bb-font-text);
  font-size: 0.82rem;
}
.shelf-rail .home-foot-btn { flex: 0 0 auto; }
/* The file route (#272) — subordinate to the ＋ plaque beside it, because it is
 * another source for the same act rather than a second verb. `flex: 0 0 auto`
 * like its neighbour, so when the rail wraps the two controls stay together on
 * the second line instead of the link stranding on a third. */
.shelf-rail .shelf-rail-file { flex: 0 0 auto; }

/* The same rail standing in for a choice tile on the empty page (#268), where
 * it is the DM's only way in. It replaces a .dialog-choice, so it takes that
 * tile's box to keep the list's rhythm — three rows in, three rows out. */
.shelf-rail-blank {
  padding: 10px 12px;
  border: 1px solid var(--bb-rule-soft);
  /* Transparent, like the tiles it stands among — MEASURED, not assumed: they
   * declare --bb-parchment-mid but compute to rgba(0,0,0,0), so the box a
   * reader sees is the roll body showing through. Giving this a real fill made
   * it lighter than its neighbours inside a bordered list, which reads as a
   * knockout — a hole where a row should be — rather than a peer. */
  background: transparent;
  /* .dialog-actions-list has gap: 0; the run's rhythm comes from each
   * .dialog-choice's own margin-top, which this doesn't inherit by standing in
   * for one. Without it the rail butts flush against the tile above. */
  margin-top: 14px;
}

/* "· from bastion.example.com" — shown only when a snapshot came from a
 * DIFFERENT deployment than the one reading it (#268). Not a warning: a
 * cross-server link is ordinary and supported, and colouring it as a hazard
 * would misread the fact.
 *
 * It carries NO weight change at all, and that is deliberate. The first attempt
 * set `font-style: italic` (a no-op — .hd-standing is already italic) plus
 * `opacity: .85`, so the only rule that did anything was the one that dropped
 * this to 4.15:1, under the 4.5:1 floor. It needs no step down: it is already
 * the last clause of the softest line on the card.
 *
 * The separator rides INSIDE the span so a wrap can't strand a dangling
 * "· from" at the end of a line. */
.hd-foreign { white-space: nowrap; }

/* ---- Key to the seals --------------------------------------------------- */

/* A footnote to the register, not a bay — the .charter-sources disclosure
 * idiom, collapsed by default so a full roll still reaches its colophon. */
.home-key-note {
  margin: 22px 0 0;
  border-top: 1px solid var(--bb-rule);
}
/* <details> carries display:block from the UA sheet; state the hidden case
 * explicitly so home.js's toggle can't lose to it under any layer ordering. */
.home-key-note[hidden] { display: none; }
.home-key-note > summary {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 10px 0;
  cursor: pointer;
  list-style: none;
}
.home-key-note > summary::-webkit-details-marker { display: none; }
.home-key-title {
  font-family: var(--bb-font-display);
  font-size: 0.78rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--bb-ink-soft);
}
/* What the summary adds that the title can't: the seal draws ONE floor, which
   is the first question a three-storey bastion's owner asks. */
.home-key-scope {
  margin-left: auto;
  font-family: var(--bb-font-italic);
  font-style: italic;
  font-size: 0.8125rem;
  color: var(--bb-ink-soft);
}
.home-key-note > summary::after {
  content: "";
  flex: none;
  width: 7px;
  height: 7px;
  margin-right: 3px;
  border-right: 1.5px solid var(--bb-ink-soft);
  border-bottom: 1.5px solid var(--bb-ink-soft);
  transform: rotate(45deg);
  transition: transform 120ms;
}
.home-key-note[open] > summary::after { transform: rotate(-135deg); }
.home-key-note > summary:hover .home-key-title,
.home-key-note > summary:hover .home-key-scope { color: var(--bb-vermillion); }
.home-key-note > summary:hover::after { border-color: var(--bb-vermillion); }
.home-key-note > summary:focus-visible {
  outline: 2px solid var(--bb-vermillion);
  outline-offset: 2px;
}

/* Borderless: the disclosure's own border-top is the rule now, and the list's
   would double it directly under the summary. */
.home-key {
  margin: 0;
  padding: 0 0 12px;
  list-style: none;
}
.home-key-row {
  display: grid;
  grid-template-columns: 28px minmax(0, 10rem) 1fr;
  align-items: baseline;
  gap: 4px 12px;
  padding: 6px 0;
}
.home-key-swatch { width: 22px; height: 16px; align-self: center; }
.home-key-term {
  font-family: var(--bb-font-display);
  font-size: 0.72rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--bb-ink);
}
.home-key-gloss {
  font-family: var(--bb-font-text);
  font-size: 0.95rem;
  line-height: 1.45;
  color: var(--bb-ink-soft);
}

/* ---- Colophon ----------------------------------------------------------- */

/* Left-aligned on the page's own left edge. Centring it was the only element on
 * the page that didn't share the column's axis, which read as a stray footer
 * from another document. */
.home-colophon {
  max-width: 820px;
  margin-inline: auto;
  padding: 0 24px 36px;
}
/* Gilt via `color`, which both the stroke and the fill inherit through
 * currentColor — one token, one declaration. */
.home-ornament {
  display: flex;
  align-items: center;
  gap: 10px;
  margin: 0 0 14px;
  color: var(--bb-gilt);
  opacity: 0.85;
}
.home-ornament .ho-rule {
  flex: 1 1 0;
  border-top: 1px solid currentColor;
  opacity: 0.5;
}
/* Flex, not a run of "·"-separated inline links: the separators were colliding
 * with the underlines and the version pill was jammed against "Source". */
.home-colophon-links {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px 16px;
  margin: 0 0 10px;
  font-family: var(--bb-font-text);
  font-size: 0.9rem;
  color: var(--bb-ink-soft);
}
.home-colophon-links a {
  color: var(--bb-vermillion);
  text-underline-offset: 3px;
}
/* Pushed clear of the links — it is build metadata, not a fifth link. */
.home-colophon-links .version-badge { margin-left: auto; }
.home-colophon-note {
  margin: 0;
  font-family: var(--bb-font-italic);
  font-style: italic;
  font-size: 0.82rem;
  color: var(--bb-ink-faint);
}

/* The theme echo (#264). `.home-foot-btn` carries the whole verb recipe —
 * display face, tracking, uppercase, hover; this adds only what a label with a
 * contained sr-only radio needs. Names, not swatches: the page is the swatch. */
.home-colophon-theme {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 6px 14px;
  margin: 0 0 12px;
}
.home-theme-btn {
  /* Contains the .sr-only radio. Without a positioned ancestor its containing
     block is the viewport, and focusing it scrolls the page to the top. */
  position: relative;
  display: inline-flex;
  align-items: baseline;
  gap: 2px;
}
/* Current, in the page's own vocabulary — the same token .holding.is-current
   .hd-name uses for "this is the one you're on". */
.home-theme-btn:has(input:checked) { color: var(--bb-vermillion-deep); }
/* Hidden, not absent: a reserved 0.8em stops the row reflowing on switch. */
.home-theme-btn:not(:has(input:checked)) .bm-dot { visibility: hidden; }
/* Without this the shared .home-foot-btn:hover lifts the current one from
   vermillion-deep to vermillion, which reads as "hovering deselects it". */
.home-theme-btn:has(input:checked):hover { color: var(--bb-vermillion-deep); }
.home-theme-btn:has(input:focus-visible) {
  outline: 2px solid var(--bb-vermillion);
  outline-offset: 2px;
}

/* ---- Responsive --------------------------------------------------------- */

@media (max-width: 980px) {
  .home-page { padding: 16px 18px 32px; }
  .holding { grid-template-columns: 72px 1fr auto; gap: 0 12px; }
  .hd-seal { width: 72px; height: 72px; }
  .hd-name { font-size: 1.2rem; }
}

@media (max-width: 640px) {
  .home-page { padding: 14px 16px 32px; }
  .home-colophon { padding: 0 16px 28px; }
  /* Drop the grid: the seal floats and the text wraps around and below it, so
     the register keeps its rhythm without a column structure to maintain. */
  .holding { display: block; padding: 12px 12px; }
  .hd-seal { float: left; width: 64px; height: 64px; margin: 0 12px 8px 0; }
  /* Right-aligned: a single ⋯ hanging at the left edge of a block row reads as
     leftover markup rather than as that row's actions. */
  .hd-verbs { clear: both; justify-content: flex-end; padding-top: 4px; }
  .home-head-aside { flex-wrap: wrap; }
  .home-filter { max-width: none; flex: 1 0 100%; }
  .home-key-row { grid-template-columns: 28px 1fr; }
  .home-key-gloss { grid-column: 2; }
  /* One verb per line below this width, as before the rail was split into runs
     — the runs go column too, so the grouping changes nothing at this tier. */
  .home-roll-foot { flex-direction: column; align-items: flex-start; gap: 10px; }
  .home-foot-run { flex-direction: column; align-items: flex-start; gap: 10px; }
  /* An auto inline margin outranks align-items in a column flex container —
     without this reset the library verbs flush right on the stacked tier. */
  .home-roll-foot:not(.is-blank) .home-foot-run + .home-foot-run { margin-left: 0; }
}

/* Any coarse pointer, at any width: collapse the verb row to a single labelled
 * overflow. Four unlabelled glyphs with no tooltip path (data-tip is
 * pointer-driven) are four mysteries on a phone; the app's own idiom is exactly
 * this fold to ⋯. The trigger stays permanently visible, so the verbs are still
 * never hover-revealed — the touch user just reaches a LABELLED menu in one tap
 * instead of guessing at glyphs. */
@media (max-width: 640px), (hover: none) {
  /* The paste field is a tap target like any other, and on the cold-DM page it
     is the FIRST one anyone touches in this product (#268) — a 24px field under
     a 44px button was the one control in that rail below the line. */
  .shelf-rail .shelf-paste { min-height: 44px; }
  /* The rail's third member, and NOT a phone gap — every <button> already
     clears 44px under (pointer: coarse) above. This query's max-width branch
     is the one that also catches touch-capable devices reporting a fine
     pointer, where the field and the plaque are 44px and this computed 23.2px:
     the odd one out in its own rail. Bare min-height, not .home-arrange's
     inline-flex — a <button> centres its own content, and the centre-to-centre
     delta against the plaque beside it is 0. `flex: 0 0 auto` stays as it is,
     so the width and therefore the wrap are unchanged. */
  .shelf-rail .shelf-rail-file { min-height: 44px; }
  /* The register filters, on the same floor as the paste field one line up —
     they are the same control at the same size, and .home-filter computes to
     26px without this. It had always been under the line on the library; #267
     put a second one on the shelf, which is what turned a single miss into a
     pattern worth fixing rather than inheriting. */
  .home-filter { min-height: 44px; }
  /* Same floor, same reason (#267): a 0.85rem italic text button sitting in a
     wrapped header row is well under it. */
  .home-arrange {
    display: inline-flex;
    align-items: center;
    min-height: 44px;
  }
  .hd-verbs .bm-clone,
  .hd-verbs .bm-export,
  .hd-verbs .bm-share { display: none; }
  .hd-verbs .bm-action {
    min-width: 44px;
    min-height: 44px;
    padding: 0;
  }
  /* The whole foot-verb family, not just the register's: these are 0.72rem
     text targets sitting in a wrapped row, which is the likeliest mis-tap on
     the page. Applied to .home-foot-btn rather than to one row so the roll's
     rail and the colophon can't drift apart. */
  .home-foot-btn {
    display: inline-flex;
    align-items: center;
    min-height: 44px;
  }
  /* Closed, the summary is ~36px — under the thumb floor. */
  .home-key-note > summary { padding: 14px 0; }
}

@media (max-width: 420px) {
  .home-continue .hc-who { display: none; }
}

@media (prefers-reduced-motion: reduce) {
  /* Hover and focus state CHANGES remain — they are information, not
     decoration. They simply arrive instantly. */
  .holding,
  .holding .bm-action { transition: none; }
}

/* ---- The wire (#266): the opt-in, and the roster dialog ----------------- */

/* The opt-in row reuses the Charter sourcebooks anatomy (.settings-row >
   label.view-toggle > checkbox) rather than inventing a control, so the tick
   and its hit area behave exactly as every other toggle in the app. Inside a
   dialog rather than the Settings dialog, so it keeps the flex recipe and needs
   its divider removed — it sits in the middle of a note stack, and a hairline
   there would read as a section break between the link and the notes about it. */
.share-optin {
  border-bottom: none;
  padding: 6px 0 2px;
}

/* One visual list for undecided and approved rows alike — the TICK is the whole
   control, and a row's own state line says why it is off. Splitting them into
   two sections would put a heading above a decision the reader makes by
   ticking, which is the shape this dialog exists to avoid. */
.wire-roster {
  max-height: 44vh;
  overflow-y: auto;
  /* The house scroll gutter: reserve the scrollbar's lane rather than letting it
     overlap the rows. */
  padding-right: 12px;
  scrollbar-gutter: stable;
  margin: 6px 0 2px;
}
.wire-row {
  gap: 10px;
}
.wire-row .view-toggle {
  /* The label is the tick's full hit area; letting it take the free space keeps
     the state line and the bin against the right edge at every width. */
  flex: 1 1 auto;
  min-width: 0;
}
.wire-row-state {
  flex: 0 1 auto;
  text-align: right;
}
.wire-row-remove {
  flex: 0 0 auto;
}
/* Below the fold the row stacks: tick + bin on one line, the state line beneath.
   A width query, not a pointer one — a narrow desktop window wraps exactly as a
   phone does, which is the distinction .share-line's own rules record. */
@media (max-width: 560px) {
  .wire-row {
    flex-wrap: wrap;
  }
  .wire-row-state {
    flex-basis: 100%;
    text-align: left;
    order: 3;
  }
}

/* The reader's ask (#266) — on the MESSAGE line, after the sentence about what
   you are looking at, because "you asked for updates" is another fact about the
   same thing. .snapshot-msg does not grow and the action row wraps below it at
   every real width, so this occupies free space and costs the band no height.

   It was a full-width row of its own first, which read as a third band and hung
   diagonally opposite the buttons it describes. Not a fifth BUTTON either — see
   .snapshot-actions, which already wraps inside itself at four. */
.snapshot-follow {
  margin: 0;
  flex: 0 1 auto;
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 6px 8px;
  font-family: var(--bb-font-italic);
  font-style: italic;
  font-size: 0.875rem;           /* 14px */
  color: var(--bb-ink-soft);
}

} /* @layer components (#259) — closes the file-wide holding layer */
