/* Design tokens (gmtool UI container, pack 1). One semantic layer —
   colour, spacing, radius, elevation, layering, type and motion —
   declared once so the rest of the container, and later tools, compose
   against it instead of re-inventing literals.

   The prefix is `--ui-*`, and the boundary inside it (container pack 2)
   is: Rust owns the colour values — Chrome::css_vars() writes the thirteen
   colour slots on <html> and this block declares their Standard
   defaults — while spacing, radius, elevation, layering, type and
   motion are CSS's alone and are never written from Rust. `--map-*` no
   longer exists. One prefix stays entirely Rust's and is read here but
   never declared: `--token-*`, set per element by swatch_style() for
   token discs and tray swatches (map_ui/tokens.rs).

   Named as if shared with charpage one day (semantic slots, a single
   accent), shipped gmtool-only: charpage sits in the Night family and
   gmtool's accent is Standard teal, so a shared file would be a shared
   base with a per-tool accent, never one flat palette. */
:root {
  /* Colour, and the one family Rust also writes. Chrome::css_vars()
     (theme.rs) sets these fourteen on <html> as inline custom properties
     when the mode changes, which outranks this block — so the values
     here are Standard's, serving as the declared default that paints
     the first frame, not a second copy of the palette. */
  --ui-surface: #15181a;
  --ui-surface-raised: #1d2226;
  --ui-ink: #e2e6e4;
  --ui-ink-muted: #93a19c;
  --ui-accent: #2f6f5e;
  --ui-edge: #37403c;
  /* Three slots that used to be the map's alone and are now the app's:
     the selection highlight, an entity-linked rim, a caption's ink. */
  --ui-select: #f0c674;
  --ui-linked: #2f6f5e;
  --ui-caption: #e2e6e4;
  /* Semantic states: Night renders them in gold and wan blood rather
     than in this amber and rose. */
  --ui-warning: #d9a05b;
  --ui-error: #d4737f;
  /* The character sheet's two (PC1): a change
     awaiting the GM, and Hunger's blood. Night's are a paler gold than
     its warning and a brighter blood than its accent — theme.rs's
     `the_sheets_colours_stay_apart` holds both apart. */
  --ui-pending: #f0c674;
  --ui-blood: #a83a45;
  /* The outline around a selected map marker's pin (marker-shapes). */
  --ui-marker-select: #f0c674;

  /* Spacing, on a 0.25rem grid: --ui-space-N is N quarter-rems. Used
     for padding, margin and gap only — never for sizing, which stays
     verbatim (the map canvas height is calibrated in rem against the
     chrome above it). */
  --ui-space-1: 0.25rem;
  --ui-space-2: 0.5rem;
  --ui-space-3: 0.75rem;
  --ui-space-4: 1rem;
  --ui-space-5: 1.25rem;
  --ui-space-6: 1.5rem;
  --ui-space-8: 2rem;
  --ui-space-10: 2.5rem;
  --ui-space-12: 3rem;

  /* Radius, deliberately in px. Several radii stay literal by design —
     the tab's `3px 3px 0 0` seam, the `999px` pill — and a corner is
     not a measure of the reader's type, so rem here would make the
     token'd corners drift away from the literal ones at a raised root. */
  --ui-radius-sm: 2px;
  --ui-radius-md: 3px;
  --ui-radius-lg: 4px;

  /* Elevation. The alphas are named on their own because
     filter: drop-shadow() takes an alpha, not a composed shadow. */
  --ui-shadow-alpha-1: 0.35;
  --ui-shadow-alpha-2: 0.6;
  --ui-shadow-alpha-3: 0.7;
  --ui-shadow-alpha-4: 0.85;
  --ui-elevation-1: 0 2px 4px rgba(0, 0, 0, var(--ui-shadow-alpha-2));
  --ui-elevation-2: 0 4px 16px rgba(0, 0, 0, var(--ui-shadow-alpha-1));

  /* The scrim (container pack 4): what an unpinned fan darkens the map
     with. On the alpha scale above because it is the same vocabulary,
     named separately because a scrim is not a shadow. */
  --ui-scrim: rgba(0, 0, 0, var(--ui-shadow-alpha-2));

  /* Layering, seven steps. MapLibre stacks its canvas, controls and
     markers inside .map-canvas: that is its own stacking context, which
     this scale neither enters nor ranks against. --ui-z-map is the
     map's place in the app, not a place among MapLibre's own layers.
     --ui-z-chrome (container pack 3) is the persistent bar and the map
     tools: above the scrim, because the clock, the quick advance and the
     due count stay usable in every state, and below the menus that open
     from them. --ui-z-tools (map-view drawers MV6) is the map tools'
     box once the GM has moved it from home: over the chrome's drawers,
     the Clock among them, which keep clear of it only at home, and still
     under the menus. */
  --ui-z-map: 0;
  --ui-z-map-overlay: 5;
  --ui-z-scrim: 10;
  --ui-z-chrome: 15;
  --ui-z-tools: 16;
  --ui-z-menu: 20;
  --ui-z-dialog: 30;

  /* Chrome geometry (container pack 3). The bar's height is the top
     inset of everything under it — the data overlay, the map tools, the dock
     and MapLibre's own top-right controls all offset by it — so it is
     declared once here. The right inset is the dock's width while the
     dock is open and zero while it is not; .shell carries that switch,
     so the value inherits down to the vendor controls inside the map. */
  --ui-chrome-top: 2.75rem;
  --ui-chrome-right: 0rem;
  --ui-dock-width: 22rem;
  /* The save banner's measured height (follow-ups sweep item 6): 0 while
     there is none; a ResizeObserver sets it inline on <html> while one
     shows, and removes it with the banner. The inline value outranks
     this and the fallback under this block. Consumers write
     calc(<base> + var(--save-banner-h)) — hence 0px, not 0: a unitless
     zero is not a length inside calc(). */
  --save-banner-h: 0px;

  /* Type, six steps against a 1rem root. The root stays relative on
     purpose: the reader's browser font size is the base, and the UI
     scale control of pack 3 writes that root rather than these. */
  --ui-text-caption: 0.75rem;
  --ui-text-small: 0.85rem;
  --ui-text-body: 1rem;
  --ui-text-lead: 1.15rem;
  --ui-text-title: 1.4rem;
  --ui-text-display: 1.6rem;
  /* Georgia is the suite's voice for prose and headings. The sans is
     for data read in columns — clocks, money, coordinates, durations —
     where Georgia's old-style figures are the wrong shape. */
  --ui-font-prose: Georgia, "Times New Roman", serif;
  --ui-font-data: system-ui, sans-serif;

  /* Motion. --ui-duration-spin is the travel spinner's revolution; the
     other two are for the container's menus. All three are zeroed
     under prefers-reduced-motion. */
  --ui-duration-fast: 120ms;
  --ui-duration-base: 200ms;
  --ui-duration-spin: 800ms;
  --ui-ease-standard: cubic-bezier(0.2, 0, 0, 1);
}

/* Until the observer has measured the banner (follow-ups sweep item 6):
   a banner of at most two lines, the assumption the offsets carried
   before the token existed. */
:root:has(.topbar .status.error) {
  --save-banner-h: 3rem;
}

/* What stays in px, and why — one note for the lot rather than one per
   line. None of it is a measure of the reader's type, so none of it
   should grow when the root font size does:
   · hairlines and 2-3px borders — a border is a drawn edge;
   · ring and shadow widths (box-shadow spreads, outline, drop-shadow) —
     they must stay hairline against whatever they ring;
   · the 640px breakpoint — a device width, not a type size;
   · pixel coordinates the Rust hands the map (map_ui/).
   Four more are called out where they sit: the tab seam, the tab's
   corner shorthand, the pill radius and the visually-hidden clip. */
* {
  box-sizing: border-box;
}

/* The root the rem scale hangs off. 100% is the reader's own browser
   font size, kept rather than clamped to the viewport. rem resolves
   against this element, not against body, so this is the declaration a
   UI scale control writes; body carries the explicit base beneath it. */
html {
  font-size: 100%;
}

body {
  margin: 0;
  background: var(--ui-surface);
  color: var(--ui-ink);
  font-family: var(--ui-font-prose);
  font-size: var(--ui-text-body);
  line-height: 1.4;
}

main {
  max-width: 60rem;
  margin: 0 auto;
  padding: var(--ui-space-6) var(--ui-space-4) var(--ui-space-12);
}

/* Both document levels are sized on the scale rather than left to the
   browser's 2em / 1.5em, which is what `.block-title` below is for at
   the third. `.topbar h1` and `.panel-title` still overrule these. */
h1 {
  font-size: var(--ui-text-display);
  font-variant: small-caps;
  letter-spacing: 0.08em;
  border-bottom: 2px solid var(--ui-accent);
  padding-bottom: var(--ui-space-1);
}

h2 {
  font-size: var(--ui-text-title);
}

/* One declaration. The shell pack's narrow-width pass added a second
   .field rule 1000 lines down; the two shared no property, so this is
   their union and computes identically. flex-wrap and max-width are
   that pass: a field wraps its control under its label rather than
   forcing horizontal page scroll at 375px. */
.field {
  display: flex;
  align-items: baseline;
  gap: var(--ui-space-2);
  flex-wrap: wrap;
  max-width: 100%;
}

/* The field label idiom, stated once. It has no class to hang off —
   every `.field > span` in the Rust is bare — so it stays a selector
   group; promoting it is a Rust edit across ~79 sites and buys nothing
   the group does not already give. */
.field > span,
.field-col > span,
.panel-title {
  color: var(--ui-ink-muted);
  text-transform: uppercase;
  font-size: var(--ui-text-small);
  letter-spacing: 0.12em;
}

/* A block's heading — the idiom that used to be six descendant
   selectors, now the class pack 1 deferred to here (container pack 5
   item 8). The descendant form reached only the blocks somebody had
   remembered to list, which is why Time's and Money's headings sat at
   the browser's 1.17em while their neighbours were on the scale.
   `.fan-head h3` keeps its seat in the group: the fan is a primitive
   pack 6 and player-view both inherit, and this pack is not its second
   author. */
.block-title,
.fan-head h3 {
  font-size: var(--ui-text-body);
  font-variant: small-caps;
  letter-spacing: 0.06em;
}

input[type="text"],
input[type="number"],
select,
textarea {
  background: var(--ui-surface-raised);
  color: var(--ui-ink);
  border: 1px solid var(--ui-edge);
  border-radius: var(--ui-radius-md);
  padding: var(--ui-space-1) var(--ui-space-2);
  font: inherit;
}

input:focus-visible,
select:focus-visible,
textarea:focus-visible,
button:focus-visible {
  outline: 2px solid var(--ui-accent);
  outline-offset: 1px;
}

button {
  font: inherit;
  background: var(--ui-surface-raised);
  color: var(--ui-ink);
  border: 1px solid var(--ui-ink-muted);
  border-radius: var(--ui-radius-md);
  /* One button padding. .rail-item, the quick-advance pair, the due
     count and the tray's add button are all <button> and take it from
     here. */
  padding: var(--ui-space-1) var(--ui-space-3);
  cursor: pointer;
}

button:hover {
  border-color: var(--ui-ink);
}

.field-col {
  display: flex;
  flex-direction: column;
  gap: var(--ui-space-1);
  margin: var(--ui-space-2) 0;
}

.field-col textarea {
  width: 100%;
  resize: vertical;
}

.field.name {
  flex: 1;
  min-width: 12rem;
}

.field.name input {
  flex: 1;
}

.limits-row {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(14rem, 1fr));
  gap: var(--ui-space-2) var(--ui-space-6);
  margin-top: var(--ui-space-2);
}

.limits-row input {
  width: 100%;
}

.containment-row {
  display: flex;
  gap: var(--ui-space-4);
  align-items: baseline;
  flex-wrap: wrap;
  margin-top: var(--ui-space-2);
}

.breadcrumb {
  margin: 0;
  color: var(--ui-ink-muted);
  font-size: var(--ui-text-small);
}

.entity-status {
  margin-top: var(--ui-space-3);
  border-top: 1px dashed var(--ui-edge);
  padding-top: var(--ui-space-2);
}

.load-line {
  margin: 0;
  font-size: var(--ui-text-small);
}

.carry-line {
  margin: var(--ui-space-1) 0 0;
  font-size: var(--ui-text-small);
  color: var(--ui-ink-muted);
}

.advisories {
  list-style: none;
  padding: 0;
  margin: var(--ui-space-1) 0 0;
}

.advisory {
  color: var(--ui-warning);
  font-size: var(--ui-text-small);
}

.advisory::before {
  content: "⚠ ";
}

.inventory {
  margin-top: var(--ui-space-3);
  border-top: 1px solid var(--ui-edge);
  padding-top: var(--ui-space-2);
}

.inventory h3 {
  margin: 0 0 var(--ui-space-2);
  color: var(--ui-ink-muted);
}

.item-new {
  display: flex;
  gap: var(--ui-space-4);
  align-items: baseline;
  flex-wrap: wrap;
  margin-bottom: var(--ui-space-2);
}

.item-list {
  list-style: none;
  padding: 0;
  margin: 0;
  display: flex;
  flex-direction: column;
  gap: var(--ui-space-2);
}

.item-row {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr));
  gap: var(--ui-space-2) var(--ui-space-4);
  border: 1px solid var(--ui-edge);
  border-radius: var(--ui-radius-md);
  padding: var(--ui-space-3) var(--ui-space-4);
}

.item-row input,
.item-row select {
  width: 100%;
  min-width: 0;
}

.item-row .field {
  flex-direction: column;
  align-items: stretch;
  gap: var(--ui-space-1);
}

.item-row .delete {
  align-self: end;
  justify-self: end;
}

/* Mods (stores C4/C5). The block spans the row it sits in — an item
   row is an auto-fit grid and a modification list is not a field. */
.mods {
  grid-column: 1 / -1;
}

.mods-toggle {
  display: inline-flex;
  align-items: baseline;
  gap: var(--ui-space-2);
  font-size: var(--ui-text-caption);
  padding: 0.1rem 0.5rem;
}

/* The count, present only when something is fitted. */
.mod-tag {
  color: var(--ui-ink-muted);
}

.mod-list {
  list-style: none;
  padding: 0;
  margin: var(--ui-space-2) 0 0;
  display: flex;
  flex-direction: column;
  gap: var(--ui-space-2);
}

.mod-row {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(8rem, 1fr));
  gap: var(--ui-space-2) var(--ui-space-4);
  border: 1px solid var(--ui-edge);
  border-radius: var(--ui-radius-md);
  padding: var(--ui-space-2) var(--ui-space-3);
}

.mod-row .field,
.mod-new .field {
  flex-direction: column;
  align-items: stretch;
  gap: var(--ui-space-1);
}

.mod-row input,
.mod-new input {
  width: 100%;
  min-width: 0;
}

.mod-row .delete {
  align-self: end;
  justify-self: end;
}

.mod-new {
  margin-top: var(--ui-space-2);
}

/* A vehicle's own modifications sit above its cargo and read as a
   block of their own — the distinction spec §5 asks for. */
.vehicle-mods {
  margin-bottom: var(--ui-space-4);
  padding-bottom: var(--ui-space-3);
  border-bottom: 1px solid var(--ui-edge);
}

.vehicle-mods .meta {
  margin: 0 0 var(--ui-space-2);
  color: var(--ui-ink-muted);
  font-size: var(--ui-text-small);
}

.unplaced {
  border: 1px solid var(--ui-accent);
  border-radius: var(--ui-radius-lg);
  padding: var(--ui-space-3) var(--ui-space-4);
  margin-top: var(--ui-space-4);
}

.status {
  min-height: 1.2em;
  font-size: var(--ui-text-small);
  color: var(--ui-ink-muted);
}

.status.error {
  color: var(--ui-error);
}

/* What the sidecar resolved (campaign-files A1b item 6): a two-column
   list of facts about where the campaign lives, above Data's Export and
   Import children. Muted like .notices — it is reference, not an
   action — and the root is a path, so it wraps rather than overflows. */
.resolved dl {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: var(--ui-space-1) var(--ui-space-4);
  margin: 0 0 var(--ui-space-4);
  font-size: var(--ui-text-small);
}

.resolved dt {
  color: var(--ui-ink-muted);
}

.resolved dd {
  margin: 0;
  overflow-wrap: anywhere;
}

.notices {
  margin-top: var(--ui-space-3);
  border-top: 1px solid var(--ui-edge);
  padding-top: var(--ui-space-2);
  color: var(--ui-ink-muted);
  font-size: var(--ui-text-small);
}

.blocked {
  border: 1px solid var(--ui-accent);
  border-radius: var(--ui-radius-lg);
  padding: var(--ui-space-4);
}

/* Pack 3: clock + reminders (additive block). */
.clock-now {
  font-size: var(--ui-text-title);
  font-weight: 600;
  margin: var(--ui-space-1) 0 var(--ui-space-3);
}

.control-row {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: var(--ui-space-3);
  margin-bottom: var(--ui-space-2);
}

.reminders h3 .count {
  margin-left: var(--ui-space-2);
  font-size: var(--ui-text-small);
  opacity: 0.7;
}

/* The transaction log's badge (container pack 6 item 8). Eleven `.count`
   badges are rendered and ten of them sit under an ancestor that styles
   them; this is the eleventh, and without a rule it read as
   "Transaction log2". Scoped to its own section like the other ten and
   not given to a bare `.count`, which would restyle ten correct badges
   to fix one. */
.money-ops h3 .count {
  margin-left: var(--ui-space-2);
  font-size: var(--ui-text-small);
  opacity: 0.7;
}

.reminder-list {
  list-style: none;
  margin: 0 0 var(--ui-space-3);
  padding: 0;
}

.reminder-row {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--ui-space-2);
  padding: var(--ui-space-1) 0;
}

.reminder-list.due .reminder-row {
  border-left: 3px solid var(--ui-accent);
  padding-left: var(--ui-space-2);
}

.reminder-at {
  font-family: var(--ui-font-data);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

.reminder-entity,
.reminder-list.dismissed .reminder-text {
  opacity: 0.7;
}

/* Map container, pack 1: the map panel (additive block). Container pack
   3: the map is the app. The stage is fixed to the viewport and the
   canvas simply fills it, so the canvas box is the viewport in every
   state and nothing above it can change its size — which is why no
   resize path exists and none is wanted.
   What went with the shell pack's viewport-derived canvas height: a
   height subtracted from the viewport was also a standing guarantee
   that the container was never 0×0. A 0×0 MapLibre container does not throw — it
   renders blank — so the Rust now measures the map's area element when
   its style is ready and reports it (map_ui/section.rs), which is the
   net this deletion removed. */
/* Map section chrome reads the --ui-* colour tokens like the rest of the
   app now (container pack 2): theme.rs writes them on <html> and the
   :root block above is their Standard default, so these rules never need
   a fallback; rules that only exist in Night sit under
   :root[data-theme="night"], the attribute the same Effect sets. Spelled
   with :root rather than bare, because :root[...] has the specificity
   .map.night had (0,2,0) and the rules kept their source order: the
   cascade is provably unchanged, which is the one thing no gate here
   could have caught. */
/* The MapLibre container: it keeps its class — the vendor cursor rules,
   the draw-trash rule and the marker tier all key off it — and fills the
   stage. No border or radius now that it is the whole window. */
.map-canvas {
  width: 100%;
  height: 100%;
  background: var(--ui-surface-raised);
}

/* The map and what is anchored over it (the travel landing menu).
   The *stage* is the fixed element, never .map-canvas: this is the
   positioned ancestor .travel-menu is absolute against, and the anchor
   it uses is a container pixel from MapHandle::project. Fix the canvas
   instead and the stage collapses to nothing while the menu re-anchors
   to the viewport corner — silently, with no error anywhere.

   THE CONTAINING-BLOCK INVARIANT, which every pack after this one
   inherits: no ancestor of this element — .map, .shell, body, html —
   may carry `transform`, `filter`, `perspective`, `backdrop-filter` or
   `will-change`. Any of them makes that ancestor the containing block
   for a fixed descendant, and `inset: 0` then means the ancestor's box
   instead of the viewport: the map silently shrinks to whatever the
   wrapper is, and every fixed surface over it moves with it. It is the
   scrim that will tempt the next editor — **darken the map with
   `background`, never with a filter on a wrapper.** Descendants are
   free (.travel-menu's own transform, the marker drop-shadows). */
.map-stage {
  position: fixed;
  inset: 0;
  z-index: var(--ui-z-map);
}

/* The map region takes focus when the app sends it "to the map" —
   "Show on map", the Map tab, Escape out of the data overlay. The ring
   is deliberately not the armed ring's 2px inset accent line: one says
   a tool is armed, this one says the keyboard is here. */
.map-stage:focus {
  outline: none;
}

.map-stage:focus-visible {
  outline: 3px dashed var(--ui-accent);
  outline-offset: -3px;
}
.travel-menu {
  position: absolute;
  z-index: var(--ui-z-map-overlay);
  /* Hangs below the anchor by the UI's own measure rather than a fixed
     12px. Off-grid on purpose: the size it rendered, now scaling. */
  transform: translate(-50%, 0.75rem);
  min-width: 14rem;
  border: 1px solid var(--ui-edge);
  border-radius: var(--ui-radius-lg);
  padding: var(--ui-space-3) var(--ui-space-4);
  background: var(--ui-surface-raised);
  color: var(--ui-ink);
  box-shadow: var(--ui-elevation-2);
  font-size: var(--ui-text-small);
}
.travel-menu h4 {
  margin: 0 0 var(--ui-space-2);
  font-size: var(--ui-text-small);
  font-weight: normal;
  color: var(--ui-ink-muted);
}
.travel-menu ul {
  list-style: none;
  margin: 0;
  padding: 0;
}
.travel-menu li + li {
  margin-top: var(--ui-space-1);
}
.travel-menu .way {
  display: flex;
  width: 100%;
  justify-content: space-between;
  gap: var(--ui-space-3);
  text-align: left;
}
.travel-menu .way .meta {
  color: var(--ui-ink-muted);
  font-size: var(--ui-text-small);
}
.travel-menu .est {
  margin-left: var(--ui-space-1);
  font-size: var(--ui-text-caption);
  color: var(--ui-ink-muted);
}
.travel-menu .way.pending {
  color: var(--ui-ink-muted);
}
.travel-menu .way.pending::after {
  content: "";
  width: 0.7rem;
  height: 0.7rem;
  border: 2px solid var(--ui-ink-muted);
  border-top-color: transparent;
  border-radius: 50%;
  animation: travel-spin var(--ui-duration-spin) linear infinite;
}
@keyframes travel-spin {
  to {
    transform: rotate(360deg);
  }
}
.travel-menu form {
  display: flex;
  gap: var(--ui-space-1);
  margin-top: var(--ui-space-2);
}
.travel-menu form input {
  flex: 1;
  min-width: 0;
}
.travel-menu .actions {
  display: flex;
  justify-content: flex-end;
  gap: var(--ui-space-2);
  margin-top: var(--ui-space-2);
}
.token-editor .travelling {
  color: var(--ui-accent);
}
.map-canvas.unavailable {
  opacity: 0.4;
}

.map .status.warning {
  color: var(--ui-warning);
}

/* The status stack (container pack 3): the four things the map can say
   — its own state, a claim pull, a notice, the 3D notice — in one live
   region below the bar. The region is always mounted and the lines are
   always in it, because a live region that appears together with its
   first message is announced by nothing. A silent line therefore keeps
   its node and drops only its card: never display:none or
   visibility:hidden, which would take it out of the accessibility tree
   and put the announcement back where it started. */
/* The save banner is fixed under the bar at --ui-z-dialog and this stack
   sits just below it, so a failing autosave would cover the map's own
   first line — the two most important messages in the app, one hiding
   the other. When the banner is present the stack drops below it by the
   banner's measured height (--save-banner-h, follow-ups sweep item 6),
   keeping the same gap under it as under the bar, however many lines
   the banner wraps to. */
.map-status-stack {
  position: fixed;
  top: calc(3.25rem + var(--save-banner-h));
  left: 0;
  right: 0;
  z-index: var(--ui-z-map-overlay);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--ui-space-2);
  padding: 0 var(--ui-space-4);
  pointer-events: none;
}

.map-status-stack .status {
  max-width: 44rem;
  margin: 0;
  padding: var(--ui-space-2) var(--ui-space-3);
  border: 1px solid var(--ui-edge);
  border-radius: var(--ui-radius-lg);
  background: var(--ui-surface);
  pointer-events: auto;
}

.map-status-stack .status.silent {
  min-height: 0;
  padding: 0;
  border: 0;
  background: none;
}

/* Attribution: one line at the foot of the map, the full prose on
   click. The ODbL credit itself is discharged in-canvas by MapLibre's
   own attribution control (attributionControl: { compact: false }), so
   what collapses here is the supplement, not the obligation. Bottom
   left: MapLibre's control has the bottom right. */
.map-attribution {
  position: fixed;
  left: var(--ui-space-2);
  bottom: var(--ui-space-2);
  z-index: var(--ui-z-map-overlay);
  max-width: 28rem;
  margin: 0;
  padding: var(--ui-space-1) var(--ui-space-2);
  border-radius: var(--ui-radius-md);
  background: var(--ui-surface);
  color: var(--ui-ink-muted);
  font-size: var(--ui-text-small);
}

.map-attribution summary {
  cursor: pointer;
}

.map-attribution p {
  margin: var(--ui-space-2) 0 0;
}

/* Armed: a tool other than Navigate is live and the whole viewport takes
   an accent ring. An inset ring on a layer of its own rather than a
   border on the stage — a border would resize the canvas, which is the
   one thing this design does not do. `pointer-events: none` because it
   covers the entire map; without it the ring would swallow every click
   it exists to announce. The 2px is a ring width and stays px. */
.map-armed {
  position: fixed;
  inset: 0;
  z-index: var(--ui-z-map-overlay);
  pointer-events: none;
  box-shadow: inset 0 0 0 2px var(--ui-accent);
}

/* The map tools (container pack 3; a box since map-view drawers MV6):
   the map's tools in one column, at home down the left edge where the
   rail was, at the bar's layer. A slim head names the box; under it the
   toolbar is one Tab stop with the arrow keys moving inside it
   (role=toolbar and a roving tabindex, map_ui/section.rs), its three
   groups labelled so a screen reader says which part you have reached.
   The box keeps to the window's height and the toolbar scrolls inside
   it, so a short window never cuts its foot off. */
.map-tools {
  position: fixed;
  left: var(--ui-space-3);
  /* Below the save banner too, by its measured height. */
  top: calc(var(--ui-chrome-top) + var(--ui-space-3) + var(--save-banner-h));
  width: 9.5rem;
  max-height: calc(100dvh - var(--ui-chrome-top) - var(--save-banner-h) - 2 * var(--ui-space-3));
  z-index: var(--ui-z-chrome);
  display: flex;
  flex-direction: column;
  border: 1px solid var(--ui-edge);
  border-radius: var(--ui-radius-lg);
  background: var(--ui-surface);
}

/* The head is what drags the box (map_ui/tools.rs): no text selection
   and no touch scroll while it does. The fold button in it stays a
   button. */
.map-tools-head {
  display: flex;
  align-items: center;
  gap: var(--ui-space-1);
  padding: var(--ui-space-1) var(--ui-space-2);
  border-bottom: 1px solid var(--ui-edge);
  color: var(--ui-ink-muted);
  font-size: var(--ui-text-caption);
  cursor: grab;
  touch-action: none;
  user-select: none;
}

.map-tools-title {
  flex: 1;
}

/* At least 24 × 24 px (WCAG 2.2, 2.5.8): a miss lands on the head, which
   drags. */
.map-tools-fold {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 1.5rem;
  min-height: 1.5rem;
  padding: 0 var(--ui-space-1);
  font-size: var(--ui-text-caption);
}

/* Moved from home, the box paints over the Clock and the top drawers
   (P6): they keep clear of it only at home, where the Clock still covers
   its edge on a short window (C12). */
.map-tools:not([data-home]) {
  z-index: var(--ui-z-tools);
}

/* The room's gauge (map_ui/tools.rs): never seen, as wide as the open
   dock and as tall as the save banner, so the box hears either change
   and keeps out from under the dock and above the window's foot. */
.map-tools-gauge {
  position: absolute;
  top: 0;
  left: 0;
  width: var(--ui-chrome-right);
  height: var(--save-banner-h);
  visibility: hidden;
  pointer-events: none;
}

/* Folded, the box is its head alone. */
.map-tools.folded > .map-toolbar {
  display: none;
}

.map-tools.folded .map-tools-head {
  border-bottom: 0;
}

.map-tools .map-toolbar {
  min-height: 0;
  overflow-y: auto;
  padding: var(--ui-space-2);
}

.map-toolbar {
  display: flex;
  flex-direction: column;
  gap: var(--ui-space-3);
}

.map-tools-group {
  display: flex;
  flex-direction: column;
  gap: var(--ui-space-1);
}

.map-toolbar button {
  width: 100%;
  text-align: left;
}

/* The armed tool. This declaration came with the buttons from the map
   toolbar, which no longer exists: it is the only thing that shows which
   tool is live, so losing it in the move would have left tools that
   never look armed while every aria-pressed stayed correct. Keyed off
   the toolbar, so it holds wherever the toolbar is mounted. */
.map-toolbar button[aria-pressed="true"] {
  background: var(--ui-accent);
  border-color: var(--ui-accent);
}

/* The scrim (container pack 4, decision A): the map darkened behind an
   unpinned fan — you are reading it. Pinned, there is no scrim and the
   map stays live — you are working it.

   The darkening is a `background` on this element and never a filter on
   a wrapper. `filter`, `transform`, `perspective`, `backdrop-filter` or
   `will-change` on any ancestor of .map-stage makes that ancestor the
   containing block for the fixed stage inside it: `inset: 0` starts
   meaning the wrapper's box rather than the viewport, the map shrinks to
   the wrapper and every fixed surface over it moves with it. Nothing
   errors and no gate here reads CSS — the map just renders wrong.

   At --ui-z-scrim it covers the stage and the hint strip and passes
   under the bar and the map tools at --ui-z-chrome, which stay usable while
   it is up, and under the fan itself at --ui-z-menu. */
.fan-scrim {
  position: fixed;
  inset: 0;
  z-index: var(--ui-z-scrim);
  background: var(--ui-scrim);
  /* Its fade lives here rather than in a second .fan-scrim block: one
     selector, one place, pack 1's rule. The motion block below still
     explains why it is a transition and not a keyframe animation, and
     holds the from-state it runs from. */
  transition: opacity var(--ui-duration-base) var(--ui-ease-standard);
}

/* The motion, for every fan (container pack 4). The two transitions it
   is about are declared with the resting rules they belong to — the
   scrim's opacity above, the panel's transform below — and this note
   and the from-states under it are what make them possible. A
   transition and not a keyframe animation, for two reasons that both
   decide it: `display` is
   not animatable, which is why .data-overlay — the only open/close
   precedent in this file — has no motion at all and is the wrong thing
   to copy; and the reduced-motion block at the foot zeroes
   --ui-duration-* on :root, which reaches transitions and *not*
   keyframes, so a keyframed fan would keep moving for a reader who
   asked it not to. There is no by-name opt-out here, and that is the
   point.

   @starting-style is what makes a transition possible at all: a fan
   mounts on open, so it enters the document already at its resting
   style and a transition has nothing to run from. These rules are that
   from-state and apply only to the frame the element is inserted on.

   Entry only. A fan unmounts on close, so there is no element left to
   transition out.

   **The box Insets measures is never the box that animates.** This is
   the invariant, and it was learned the expensive way: the first
   version of this block slid .map-dock itself, and .map-dock is what
   covering_rect() asks for. getBoundingClientRect() *includes*
   transforms, so one frame into the slide the camera read a dock that
   was still off-stage, floored the right band to zero and framed 176px
   out — worse than the 118px bug pack 3 fixed. Deferring the
   measurement further is not the answer: prefers-reduced-motion zeroes
   these durations, so the wait would have to be read back out of CSS,
   and every later animated surface would have to remember to do it. So
   the fan's geometry and its motion are two elements — .map-dock is a
   fixed frame at its resting size that never moves, and .fan-panel is
   the surface that paints and slides inside it.

   The transform is therefore on .fan-panel, a descendant of .map-dock,
   which is itself a fixed *sibling* of .map-stage and never an ancestor
   of it — the containing-block invariant is untouched either way. */

/* The surface every fan paints, scrolls and slides with, inside the
   frame its tenant class provides (.map-dock below; .view-menu further
   down). A later fan inherits the whole of the motion by rendering
   through the component.

   It sits **above** @starting-style and not below it, which is where
   the transition-only block it absorbed used to be: the resting rules
   come first and the from-states follow them, in the order they always
   did. There is no need to find out whether a transition declared after
   its own from-state block still runs — a fan that appears instead of
   sliding is invisible to every gate in this repo. */
.fan-panel {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: var(--ui-space-2);
  padding: var(--ui-space-3);
  overflow: auto;
  background: var(--ui-surface);
  /* The slide, declared with the surface it moves rather than in a
     second .fan-panel block. The transform is on this element and never
     on .map-dock, which is the box covering_rect() measures — see the
     motion block above. */
  transition: transform var(--ui-duration-base) var(--ui-ease-standard);
}

/* Which way a fan comes in is which edge it is anchored to, so the
   from-state belongs to the tenant's class rather than to .fan-panel.

   **This block is load-bearing and stands alone on purpose.** Folding
   these declarations into the resting rules above does not tidy the
   file, it deletes the fan's entry motion — a fan mounts on open, so it
   enters the document already at its resting style and a transition has
   nothing to run from. Nothing would fail: it compiles, it builds, and
   the fan simply appears. */
@starting-style {
  .map-dock .fan-panel {
    transform: translateX(100%);
  }

  .fan-scrim {
    opacity: 0;
  }
}

/* The dock (container pack 3): one door at a time at the right edge —
   the selected thing's editor, the annotation list or the token tray,
   each exactly the component that stood in the section before. It is
   mounted only while open, so a closed dock is out of the tab order and
   measures zero when the camera asks what is covering the map.

   This element is the dock's *frame*: where it is, how wide it is, and
   which layer it is on — and nothing that can move it. It is the box
   covering_rect(".map-dock") reads, so a transform here would be a
   measurement the camera cannot trust (see the motion block above). */
.map-dock {
  position: fixed;
  top: var(--ui-chrome-top);
  right: 0;
  bottom: 0;
  width: var(--ui-dock-width);
  z-index: var(--ui-z-menu);
  display: flex;
}

/* The edge the fan is anchored to is the tenant's, like the direction it
   slides from. */
.map-dock .fan-panel {
  border-left: 1px solid var(--ui-edge);
}

.fan-head {
  display: flex;
  align-items: baseline;
  gap: var(--ui-space-2);
}

/* The heading takes the shared section-head typography above; the
   margin it would otherwise inherit from the browser is what would push
   the dock's controls out of line with it. */
.fan-head h3 {
  margin: 0;
}

/* The free space goes to whichever control comes first after the
   heading: the pin where there is one, and Close where there is not, so
   a pin-less fan puts Close at the right edge of its head rather than
   beside the title. One rule and not two, because two would both take
   `margin-left: auto` on a fan that has a pin and split the gap between
   them. */
.fan-head h3 + button {
  margin-left: auto;
}

.map-dock button[aria-pressed="true"] {
  background: var(--ui-accent);
  border-color: var(--ui-accent);
}

/* The dock's first child sets its own top margin; inside a panel of its
   own the editors and lists do not need the separation they had when
   they stacked under the map. */
.map-dock .map-editor,
.map-dock .annotation-list {
  margin-top: 0;
}

/* The View menu: how the map is drawn and what it shows, hanging from
   the bar at the same layer as the dock — one of them is open at most
   (decision 2), so they may share a layer and a corner. It is the fan
   primitive's second tenant (container pack 6), so it is split the way
   .map-dock is: this element is the *frame* — where it is, how wide it
   is, which layer it is on — and the .fan-panel inside it is
   everything that paints. The split is the primitive's rule, not this
   menu's preference: the root is the box anything measuring a fan
   reads, so it must never move. */
.view-menu {
  position: fixed;
  /* Below the save banner too, by its measured height. The dock does not
     follow: the camera measures it (.map-dock's comment). */
  top: calc(var(--ui-chrome-top) + var(--save-banner-h));
  right: var(--ui-space-3);
  width: 14rem;
  z-index: var(--ui-z-menu);
  display: flex;
}

/* A menu hangs in the air where the dock is a wall, so its panel keeps
   the card treatment the fixed div carried itself: an edge all round, a
   corner and a shadow. The column, the gap, the padding and the surface
   come from .fan-panel. */
.view-menu .fan-panel {
  border: 1px solid var(--ui-edge);
  border-radius: var(--ui-radius-lg);
  box-shadow: var(--ui-elevation-2);
}

.view-menu button[aria-pressed="true"] {
  background: var(--ui-accent);
  border-color: var(--ui-accent);
}

/* The hint strip: the armed tool's instruction at the foot of the map,
   with the actions that finish a drawing beside it. It spans the window
   so its contents can centre, which is why the strip itself takes no
   pointer events — a band across the map that swallowed drags would be
   a strange way to explain the map. Its children take them back.
   It is always shown, lifted above the map tools' bottom drawer (P9)
   by how far up that drawer reaches, its tab alone while it is in. */
.map-hint-strip {
  position: fixed;
  bottom: calc(var(--ui-space-8) + var(--drawer-bottom-reach, 0px));
  left: 0;
  right: 0;
  z-index: var(--ui-z-map-overlay);
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--ui-space-3);
  padding: 0 var(--ui-space-4);
  pointer-events: none;
}

/* While it holds a draft's actions (Finish zone, Cancel, Done editing
   shape) the strip rises over a moved map tools box, which the GM may
   have dropped across the foot, and stays under the menus and the dock. */
.map-hint-strip:has(.draft-actions) {
  z-index: calc(var(--ui-z-tools) + 1);
}

.map-hint-strip > * {
  pointer-events: auto;
}

/* Legible over tiles: the hint is a card of its own now, not a line
   under a toolbar. */
.map-hint-strip .map-hint {
  max-width: 44rem;
  padding: var(--ui-space-2) var(--ui-space-3);
  border: 1px solid var(--ui-edge);
  border-radius: var(--ui-radius-lg);
  background: var(--ui-surface);
}

.map-hint-strip .draft-actions {
  padding: var(--ui-space-2) var(--ui-space-3);
  border: 1px solid var(--ui-edge);
  border-radius: var(--ui-radius-lg);
  background: var(--ui-surface);
}

/* "Claim this area?" — a card centred over the map. It is none of the
   dock's three doors (it is a question about the ring just drawn, not a
   thing selected), and with the stage fixed there is no section flow
   left to stand in, so it gets a surface of its own. Centred with auto
   margins rather than a transform: a transform here is harmless, but the
   habit of reaching for one is what breaks the fixed stage the day it
   lands on an ancestor instead (see .map-stage). */
.claim-pending {
  position: fixed;
  top: 6rem;
  left: 0;
  right: 0;
  width: 26rem;
  max-width: calc(100% - var(--ui-space-8));
  margin: 0 auto;
  z-index: var(--ui-z-menu);
  box-shadow: var(--ui-elevation-2);
}

/* The map's own chrome stands aside for the data UI. The stage never
   joins this list — hiding the mounted canvas is what collapses it. */
.shell.data-open .map-tools,
.shell.data-open .map-dock,
.shell.data-open .fan-scrim,
.shell.data-open .view-menu,
.shell.data-open .drawer,
.shell.data-open .claim-pending {
  display: none;
}

.map-hint {
  margin: 0;
  color: var(--ui-ink-muted);
  font-size: var(--ui-text-small);
}

.draft-actions {
  display: inline-flex;
  gap: var(--ui-space-2);
  align-items: center;
}

.map-canvas.placing .maplibregl-canvas-container.maplibregl-interactive {
  cursor: crosshair;
}

/* Map container, pack 2: mapbox-gl-draw's cursor rules re-stated for
   MapLibre's class prefixes (vendor/mapbox-gl-draw/README.md). The
   plugin toggles mouse-*/mode-*/feature-* on the map container. */
.maplibregl-map.mouse-pointer .maplibregl-canvas-container.maplibregl-interactive {
  cursor: pointer;
}

.maplibregl-map.mouse-move .maplibregl-canvas-container.maplibregl-interactive,
.maplibregl-map.mode-direct_select.feature-vertex.mouse-move .maplibregl-canvas-container.maplibregl-interactive,
.maplibregl-map.mode-direct_select.feature-feature.mouse-move .maplibregl-canvas-container.maplibregl-interactive {
  cursor: move;
}

.maplibregl-map.mouse-add .maplibregl-canvas-container.maplibregl-interactive {
  cursor: crosshair;
}

.maplibregl-map.mouse-move.mode-direct_select .maplibregl-canvas-container.maplibregl-interactive,
.maplibregl-map.mode-static.mouse-pointer .maplibregl-canvas-container.maplibregl-interactive {
  cursor: grab;
}

.maplibregl-map.mode-direct_select.feature-midpoint.mouse-pointer .maplibregl-canvas-container.maplibregl-interactive {
  cursor: cell;
}

/* The two vendor surfaces that collide with this pack's chrome: the
   NavigationControl at "top-right" and MapLibre's own non-compact
   attribution at bottom right — both inside the canvas, both positioned
   by the vendor stylesheet at top/right/bottom 0 (the 10px everyone
   sees is a margin on the inner .maplibregl-ctrl). These selectors are
   the vendor's own, so they win on order: index.html loads style.css
   after maplibre-gl.css. The geometry comes from the properties the bar
   and the dock publish, which are declared on :root, switched on
   .shell.dock-open, and inherit all the way down here. */
.maplibregl-ctrl-top-right {
  top: var(--ui-chrome-top);
  right: var(--ui-chrome-right);
}

.maplibregl-ctrl-bottom-right {
  right: var(--ui-chrome-right);
}

/* The trash control exists only so Delete removes a selected vertex;
   the map toolbar is the visible UI. */
.map-canvas .mapbox-gl-draw_ctrl-draw-btn {
  display: none;
}

/* Marker DOM elements live inside MapLibre's canvas container. The
   element's box is the pin alone, so with anchor "bottom" the pin's tip
   is on the coordinate; the caption hangs under the foot at every zoom. */
.gm-marker {
  display: flex;
  flex-direction: column;
  align-items: center;
  cursor: pointer;
  user-select: none;
  font-family: var(--ui-font-data);
}

.gm-marker .glyph {
  display: block;
  line-height: 0;
}

/* A marker's pin (marker-shapes, `map_ui/pins.rs`): the kind's shape on
   a small foot, a rim and a line icon in the kind's ink (the element's
   `color`), filled with the raised surface. 32 x 38 view box units. */
.pin {
  display: block;
  width: 2rem;
  height: 2.375rem;
  overflow: visible;
  filter: drop-shadow(0 1px 2px rgba(0, 0, 0, var(--ui-shadow-alpha-3)));
}

/* The selection outline and its dark gap ring show on a selected marker
   only: place and loot already have a pale gold rim, which the gap keeps
   apart from the outline. */
.pin .pin-outline,
.pin .pin-gap {
  display: none;
}

.gm-marker.selected .pin .pin-outline,
.gm-marker.selected .pin .pin-gap {
  display: inline;
}

.gm-marker .caption {
  position: absolute;
  top: 100%;
  left: 50%;
  transform: translateX(-50%);
  margin-top: 0.125rem;
  font-size: var(--ui-text-caption);
  line-height: 1.3;
  background: color-mix(in srgb, var(--ui-surface) 85%, transparent);
  color: var(--ui-caption);
  padding: 0 var(--ui-space-1);
  border-radius: var(--ui-radius-sm);
  white-space: nowrap;
  max-width: 10rem;
  overflow: hidden;
  text-overflow: ellipsis;
}

.gm-marker .caption:empty {
  display: none;
}

/* An entity-linked marker shows it here only: its pin's rim is the
   kind's ink, and npc's is already old gold. */
.gm-marker.linked .caption {
  border-bottom: 2px solid var(--ui-linked);
}

/* Night: the caption and the foot carry a blood rim. */
:root[data-theme="night"] .gm-marker .caption {
  box-shadow: 0 0 0 1px var(--ui-accent);
}

:root[data-theme="night"] .map-canvas.table .gm-marker.sprited .glyph {
  box-shadow: 0 0 0 2px var(--ui-accent);
}

/* 3D tier, pack 3: at table zoom the sprite layer carries the pin
   (upright over the tilted map); the DOM marker keeps a foot dot centred
   on the coordinate, under the sprite's tip, as the click / drag handle,
   caption hung beneath it. */
.map-canvas.table .gm-marker.sprited .glyph {
  /* The foot at the coordinate: the 10px it was, now scaling. */
  width: 0.625rem;
  height: 0.625rem;
  border-radius: 50%;
  transform: translateY(50%);
  background: color-mix(in srgb, var(--ui-select) 90%, transparent);
  box-shadow: 0 0 0 2px color-mix(in srgb, var(--ui-surface) 85%, transparent);
}

.map-canvas.table .gm-marker.sprited .pin {
  display: none;
}

.map-canvas.table .gm-marker.sprited.selected .glyph {
  background: var(--ui-accent);
}

/* Under the half of the foot dot that hangs below the coordinate. */
.map-canvas.table .gm-marker.sprited .caption {
  margin-top: 0.4375rem;
}

/* Play tokens (map-tokens pack): a DOM marker at every zoom, never a
   sprite, so it stays draggable and upright under tilt (decision 6).
   The root is the disc's box, centred on the coordinate by MapLibre
   (anchor center); the disc child carries the palette colours as the
   --token-* variables style_token sets on it; the caption hangs below,
   out of flow, so it does not shift the anchor. */
.gm-token {
  /* The disc is DOM, not a sprite, so it scales with the UI: the 28px
     it was, in rem. Its rings stay px, which is the intent — at a
     raised root the disc grows and the rim stays hairline. */
  width: 1.75rem;
  height: 1.75rem;
  cursor: grab;
  user-select: none;
  font-family: var(--ui-font-data);
}

.gm-token:active {
  cursor: grabbing;
}

.gm-token .disc {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  height: 100%;
  border-radius: 50%;
  background: var(--token-fill);
  color: var(--token-ink);
  font-size: var(--ui-text-caption);
  font-weight: 700;
  letter-spacing: 0.02em;
  line-height: 1;
  box-shadow: 0 0 0 2px var(--ui-surface), 0 2px 4px rgba(0, 0, 0, 0.6);
}

.gm-token.linked .disc {
  box-shadow: 0 0 0 2px var(--ui-linked), 0 2px 4px rgba(0, 0, 0, 0.6);
}

.gm-token.selected .disc {
  box-shadow: 0 0 0 2px var(--ui-surface), 0 0 0 4px var(--ui-select), 0 2px 4px rgba(0, 0, 0, 0.6);
}

.gm-token.selected.linked .disc {
  box-shadow: 0 0 0 2px var(--ui-linked), 0 0 0 4px var(--ui-select), 0 2px 4px rgba(0, 0, 0, 0.6);
}

.gm-token .caption {
  position: absolute;
  top: 100%;
  left: 50%;
  transform: translateX(-50%);
  margin-top: 0.1875rem;
  font-size: var(--ui-text-caption);
  line-height: 1.3;
  background: color-mix(in srgb, var(--ui-surface) 85%, transparent);
  color: var(--ui-caption);
  padding: 0 var(--ui-space-1);
  border-radius: var(--ui-radius-sm);
  white-space: nowrap;
  max-width: 10rem;
  overflow: hidden;
  text-overflow: ellipsis;
}

.gm-token .caption:empty {
  display: none;
}

.gm-token.linked .caption {
  border-bottom: 2px solid var(--ui-linked);
}

/* Night: the disc rim is blood (old gold when linked), the selection
   ring bright blood, the caption carries the blood hairline markers get. */
:root[data-theme="night"] .gm-token .disc {
  box-shadow: 0 0 0 2px var(--ui-accent), 0 2px 5px rgba(0, 0, 0, 0.85);
}

:root[data-theme="night"] .gm-token.linked .disc {
  box-shadow: 0 0 0 2px var(--ui-linked), 0 2px 5px rgba(0, 0, 0, 0.85);
}

:root[data-theme="night"] .gm-token.selected .disc {
  box-shadow: 0 0 0 2px var(--ui-surface), 0 0 0 4px var(--ui-select), 0 2px 5px rgba(0, 0, 0, 0.85);
}

:root[data-theme="night"] .gm-token.selected.linked .disc {
  box-shadow: 0 0 0 2px var(--ui-linked), 0 0 0 4px var(--ui-select), 0 2px 5px rgba(0, 0, 0, 0.85);
}

:root[data-theme="night"] .gm-token .caption {
  box-shadow: 0 0 0 1px var(--ui-accent);
}

/* Token tray and editor: the swatch is the disc in miniature, drawn from
   the same --token-* variables; in the editor the swatches are buttons
   and the pressed one wears the selection ring. */
.token-tray h3 {
  display: flex;
  align-items: baseline;
  gap: var(--ui-space-2);
}

.token-tray h3 .add {
  margin-left: auto;
  font-variant: normal;
  letter-spacing: normal;
  font-size: var(--ui-text-small);
}

.token-swatch {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: none;
  width: 1.6rem;
  height: 1.6rem;
  padding: 0;
  border: none;
  border-radius: 50%;
  background: var(--token-fill);
  color: var(--token-ink);
  font-family: var(--ui-font-data);
  font-size: var(--ui-text-caption);
  font-weight: 700;
  line-height: 1;
  box-shadow: 0 0 0 2px var(--ui-surface);
}

.token-swatch.linked {
  box-shadow: 0 0 0 2px var(--ui-linked);
}

.annotation-row .token-swatch {
  align-self: center;
}

.token-palette {
  display: flex;
  flex-wrap: wrap;
  gap: var(--ui-space-2);
  padding: var(--ui-space-1) var(--ui-space-1);
}

button.token-swatch {
  cursor: pointer;
}

button.token-swatch:hover {
  box-shadow: 0 0 0 2px var(--ui-ink);
}

button.token-swatch[aria-pressed="true"] {
  box-shadow: 0 0 0 2px var(--ui-surface), 0 0 0 4px var(--ui-select);
}

button.token-swatch:disabled {
  cursor: not-allowed;
}

.token-editor .move[aria-pressed="true"] {
  background: var(--ui-accent);
  border-color: var(--ui-accent);
}

.map-editor {
  border: 1px solid var(--ui-edge);
  border-radius: var(--ui-radius-lg);
  padding: var(--ui-space-3) var(--ui-space-4);
  margin-top: var(--ui-space-2);
  background: var(--ui-surface-raised);
}

.map-editor-head {
  display: flex;
  gap: var(--ui-space-4);
  align-items: baseline;
  flex-wrap: wrap;
}

.map-editor-head h3 {
  margin: 0;
}

.map-editor-head .coords {
  color: var(--ui-ink-muted);
  font-size: var(--ui-text-small);
  font-family: var(--ui-font-data);
  font-variant-numeric: tabular-nums;
}

.map-editor-head .delete {
  margin-left: auto;
}

.map-editor-row {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(14rem, 1fr));
  gap: var(--ui-space-2) var(--ui-space-6);
  margin-top: var(--ui-space-2);
}

.map-editor-row input,
.map-editor-row select {
  width: 100%;
}

/* A unit belongs to the figure beside it, not to a field of its own.
   `.field > span` dresses captions, so the base-currency name on the
   area-facts income box has to opt out or it reads as a fifth
   control with nothing to type into. */
.field > span.unit {
  text-transform: none;
  letter-spacing: normal;
  opacity: 0.75;
}

.annotation-list {
  margin-top: var(--ui-space-3);
}

.annotation-list h3 {
  margin: 0 0 var(--ui-space-2);
  color: var(--ui-ink-muted);
}

.annotation-list h3 .count {
  margin-left: var(--ui-space-2);
  font-size: var(--ui-text-small);
  opacity: 0.7;
}

.annotation-list ul {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--ui-space-1);
}

.annotation-row {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--ui-space-2);
  padding: var(--ui-space-1) var(--ui-space-2);
  border: 1px solid transparent;
  border-radius: var(--ui-radius-md);
}

.annotation-row.selected {
  border-color: var(--ui-accent);
}

.annotation-row .glyph {
  width: 1.5rem;
  text-align: center;
}

/* A marker row's small pin (marker-shapes), centred in the glyph's
   column. */
.annotation-row .glyph .pin {
  width: 1rem;
  height: 1.1875rem;
  margin-inline: auto;
  filter: none;
}

.annotation-row .name {
  flex: 1;
  min-width: 8rem;
  background: none;
  border: none;
  padding: 0;
  text-align: left;
  cursor: pointer;
}

.annotation-row .name.missing {
  color: var(--ui-ink-muted);
  font-style: italic;
  cursor: default;
}

.annotation-row .name.missing:hover {
  text-decoration: none;
}

.annotation-row .name:hover {
  text-decoration: underline;
}

.annotation-row .meta {
  color: var(--ui-ink-muted);
  font-size: var(--ui-text-small);
}

/* Sub-zones: zone rows are a tree, indented by depth (`--depth`, set
   on the row), with a fold mark, a digit on every row, and a zone too
   small to show at this zoom drawn muted. */
.annotation-row {
  padding-left: calc(var(--ui-space-2) + var(--depth, 0) * 1.1rem);
}

.annotation-row .digit {
  min-width: 1.4rem;
  color: var(--ui-ink-muted);
  font-variant-numeric: tabular-nums;
  font-size: var(--ui-text-small);
  text-align: right;
}

.annotation-row .twisty {
  width: 0.9rem;
  color: var(--ui-ink-muted);
  cursor: pointer;
  user-select: none;
}

.annotation-row.muted .name,
.annotation-row.muted .glyph {
  color: var(--ui-ink-muted);
  opacity: 0.7;
}

/* The zone editor's place among zones: its ancestors as links, and
   its sub-zones as buttons. */
.zone-crumbs {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--ui-space-1);
  margin: var(--ui-space-1) 0 var(--ui-space-2);
  color: var(--ui-ink-muted);
  font-size: var(--ui-text-small);
}

.zone-crumbs button {
  background: none;
  border: none;
  padding: 0;
  color: inherit;
  cursor: pointer;
}

.zone-crumbs button:hover {
  text-decoration: underline;
}

.zone-crumbs b {
  color: var(--ui-ink);
}

.zone-children-list {
  display: flex;
  flex-wrap: wrap;
  gap: var(--ui-space-1);
}

/* Shell: the persistent bar, the data overlay and, inside it, the tab
   bar and panels the 2026-09-04 pack shipped (container pack 3).

   The shell is a frame for fixed surfaces now — the map is the page —
   so it drops `main`'s reading column. The overlay keeps one. */
.shell {
  max-width: none;
  margin: 0;
  padding: 0;
}

/* The dock occludes the right edge of the map while it is open. Declared
   on the shell so it inherits all the way down to MapLibre's own
   controls inside the canvas, which have to step out from under it. */
.shell.dock-open {
  --ui-chrome-right: var(--ui-dock-width);
}

/* …and the data overlay takes it back (container pack 4). The overlay
   hides the dock with `display: none`, so with both classes on the shell
   the rule above would still publish 22rem of drawer and MapLibre's own
   controls would step out from under one that is not there. The two
   selectors have the same specificity, so it is this rule's position
   after that one — and nothing else — that decides which wins. */
.shell.data-open {
  --ui-chrome-right: 0rem;
}

/* The bar: fixed, opaque, one row. Opaque because the map runs under it
   at full size and a translucent bar over moving tiles is unreadable;
   one row because it may not wrap down over the map.

   **One row means one line per control, too.** The bar is a fixed
   2.75rem, and a control that wraps its text is taller than that: the
   GM's bar needs ~1310px unwrapped, so below that every item used to
   shrink to its min-content — the clock went to three lines, "+1 h" and
   "World data" to two (a two-line button is 48px in a 44px bar), and
   "View as" wrapped above its own <select> and rose to y = −7, off the
   top of the window (2026-09-23 walkthrough). So nothing in the bar
   shrinks or wraps, and two things give instead, in this order:

   1. the wordmark, which goes whole rather than being clipped (below);
   2. then the bar itself scrolls sideways, so a control past the right
      edge is still one scroll — or one Tab, which scrolls it into view
      — away, rather than off the window with no way to it. It was: at
      800px, Tokens, View and World data were simply gone.

   A few surfaces opt back in as the elastic one, each where it is
   declared: a seat's page gives its chip (.view-as), the play surface
   its seat line (.player-seat).

   The overflow does not clip the autosave banner: it is `position:
   fixed`, so its containing block is the viewport and not this box. */
.topbar {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  height: var(--ui-chrome-top);
  z-index: var(--ui-z-chrome);
  display: flex;
  flex-wrap: nowrap;
  align-items: center;
  gap: var(--ui-space-2) var(--ui-space-4);
  padding: 0 var(--ui-space-3);
  overflow-x: auto;
  overflow-y: hidden;
  overscroll-behavior-x: contain;
  scrollbar-width: thin;
  background: var(--ui-surface);
  border-bottom: 2px solid var(--ui-accent);
}

.topbar > * {
  flex-shrink: 0;
}

/* The wordmark is the bar's first give, and it goes whole: a clipped
   "GMT…" reads as a fault. It is a wrapping flex row whose first item
   is an empty ::before as tall as the bar, so while the wordmark fits
   beside it, it sits on that first line; the moment the bar is short
   of room this box shrinks (a shrink factor far above anything else in
   the bar, so it goes first), the text wraps onto a second line, and
   `overflow: hidden` clips that line away. Still in the document, so
   the page keeps its heading. */
.topbar h1 {
  flex: 0 1000 auto;
  min-width: 0;
  align-self: stretch;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  overflow: hidden;
  margin: 0;
  border-bottom: none;
  padding-bottom: 0;
  font-size: var(--ui-text-title);
  white-space: nowrap;
}

.topbar h1::before {
  content: "";
  height: 100%;
}

/* The map's own controls sit at the far end of the bar. */
.bar-map {
  display: flex;
  align-items: center;
  gap: var(--ui-space-2);
  margin-left: auto;
}

/* Armed state for the bar's toggles, the idiom the map toolbar uses. */
.topbar button[aria-pressed="true"] {
  background: var(--ui-accent);
  border-color: var(--ui-accent);
}

.topbar button:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

/* The data UI (decision 5): it overlays the map, which keeps rendering
   underneath at its own size. Below the bar in the stack so every bar
   control still works with it open, and it hides the map's own chrome
   (rail, dock) itself rather than competing with it. */
.data-overlay {
  position: fixed;
  top: var(--ui-chrome-top);
  left: 0;
  right: 0;
  bottom: 0;
  z-index: var(--ui-z-scrim);
  display: none;
  overflow: auto;
  padding: var(--ui-space-4) var(--ui-space-4) var(--ui-space-12);
  background: var(--ui-surface);
}

/* The save banner is fixed under the bar at --ui-z-dialog, above this
   overlay, and it covered the rail's first destination whenever it
   showed (F2b, reproduced with the sidecar stopped: at 1280px the
   banner ran 44–80px and "World" 60–92px). The same height as
   .map-status-stack clears, but spent inside the overlay as a
   border rather than by moving its top: moved, the overlay uncovered a
   12px strip of map between the banner and itself, a map control
   showing through it. The border is the overlay's own surface, so
   nothing shows through, and it lies outside the scrollport, so the
   sticky rail stops below it once scrolled too. There is one banner
   (follow-ups sweep item 6), and the border is its measured height
   (--save-banner-h), so a three-line one — the not-running sentence
   below 330px wide, or a long refusal in the sidecar's own words —
   clears "World" as a one-line one does. */
:root:has(.topbar .status.error) .data-overlay {
  border-top: calc(0px + var(--save-banner-h)) solid var(--ui-surface);
}

/* Open, it is two columns: the rail and the reading column beside it.
   The centring the strip used to get from `.data-overlay > *` now
   belongs to `.panels` alone — the rail is pinned left. */
.data-overlay.open {
  display: flex;
  align-items: flex-start;
  gap: var(--ui-space-6);
  /* The right padding matches the gap, and that is what makes
     `.panels`' auto margins read as centred (container pack 6 item 12).
     The space left of the column is the gap plus its margin; the space
     right of it is its margin plus this padding. Equal margins only
     look equal when the two constants they sit beside are equal — at
     1rem here the column landed 4px right of centre, which is inside
     nobody's tolerance but is the kind of thing that gets re-reported
     as a bug. */
  padding-right: var(--ui-space-6);
}

/* The rail (container pack 5). `.data-rail` and not `.rail`: the map's
   own tools were once a left rail too, and are `display: none` under
   `.shell.data-open`, which is exactly when this one has to be visible.
   Sticky rather than fixed so it scrolls with a long section only once
   its own height is exhausted, and so it never becomes a containing
   block for anything inside the overlay. */
.data-rail {
  flex: 0 0 11rem;
  position: sticky;
  top: 0;
  border-right: 1px solid var(--ui-edge);
  padding-right: var(--ui-space-3);
}

.data-rail ul {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--ui-space-1);
}

/* A destination, not a tab: full width, left-aligned, and marked by a
   filled edge rather than by joining a strip's border seam. */
.rail-item {
  display: block;
  width: 100%;
  text-align: left;
  background: none;
  border: 1px solid transparent;
  border-left: 3px solid transparent;
  border-radius: var(--ui-radius-md);
  color: var(--ui-ink-muted);
}

.rail-item:hover {
  color: var(--ui-ink);
  border-color: transparent;
  border-left-color: var(--ui-edge);
}

.rail-item.current {
  color: var(--ui-ink);
  background: var(--ui-surface-raised);
  border-left-color: var(--ui-accent);
}

/* Data's children (item 7). Nesting is presentational — they are
   in-page navigation inside one section, not two more destinations —
   so they are indented rather than given a rule of their own. */
.rail-children {
  list-style: none;
  margin: var(--ui-space-1) 0 0;
  padding: 0 0 0 var(--ui-space-3);
  display: flex;
  flex-direction: column;
  gap: var(--ui-space-1);
}

.rail-item.child {
  font-size: var(--ui-text-small);
}

/* The focus target after a section switch (keyboard users land here). */
.panel-title {
  margin: 0 0 var(--ui-space-2);
  font-weight: normal;
}

.panel-title:focus-visible {
  outline: 2px solid var(--ui-accent);
  outline-offset: 2px;
}

/* Centred in the space right of the rail, not in the viewport (container
   pack 6 item 12, answering the question pack 5 left as the user's). The
   column keeps its width and its content: auto margins take the free
   space the 60rem cap leaves and split it in two, so the rail stays
   pinned left and what is beside it is centred beside it. Centring in
   the viewport instead would push the column visibly off-centre by half
   the rail; widening the reading column would change line length, which
   is not what was asked. At 1440 with the rail at its default width:
   .panels spanned x 216-1176 with 248px dead to its right, and now
   spans x 336-1296 with 144px either side of it. */
.panels {
  position: relative;
  margin-inline: auto;
  flex: 1 1 auto;
  /* The reading column, kept off the rail's flex basis: `min-width: 0`
     so a wide child (an export textarea, a map list row) shrinks rather
     than pushing the rail off-screen. */
  min-width: 0;
  max-width: 60rem;
}

/* Inactive panels stay mounted and laid out at full width, just
   invisible and out of the flow: display:none would collapse the
   MapLibre canvas to 0×0 and bring it back blank (pack risk note). */
.panel:not(.active) {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  height: 0;
  overflow: hidden;
  visibility: hidden;
}

.topbar-clock {
  margin: 0;
  font-weight: 600;
  font-family: var(--ui-font-data);
  font-variant-numeric: tabular-nums;
}

.quick-advance {
  display: inline-flex;
  gap: var(--ui-space-1);
}

.topbar button {
  font-size: var(--ui-text-small);
}


.due .count {
  display: inline-block;
  min-width: 1.5em;
  margin-left: var(--ui-space-1);
  padding: 0 0.35em;
  border-radius: 1em;
  background: var(--ui-edge);
  text-align: center;
  font-family: var(--ui-font-data);
  font-variant-numeric: tabular-nums;
}

.due.has-due .count {
  background: var(--ui-accent);
}

/* The autosave-failure banner. Full width directly under the bar and at
   --ui-z-dialog: in a browser-storage-only app this is the one message
   that must never end up behind a menu, the dock or the data overlay.
   One paragraph for both halves (app::SaveBanner), whose height a
   ResizeObserver writes to --save-banner-h for the offsets below it. */
.topbar .status.error {
  position: fixed;
  top: var(--ui-chrome-top);
  left: 0;
  right: 0;
  z-index: var(--ui-z-dialog);
  margin: 0;
  padding: var(--ui-space-2) var(--ui-space-3);
  background: var(--ui-surface-raised);
  border-bottom: 1px solid var(--ui-error);
}

/* Narrow-width pass (shell pack): no control grows past its row — a
   select is as wide as its longest option (entity names) otherwise.
   The .field half of this pass now sits with .field's own declaration. */
input,
select,
textarea {
  max-width: 100%;
  min-width: 0;
}

@media (max-width: 640px) {
  .topbar {
    gap: var(--ui-space-1) var(--ui-space-3);
  }

  .rail-item {
    padding: var(--ui-space-1) var(--ui-space-2);
  }

  .unplaced,
  .map-editor {
    padding-left: var(--ui-space-2);
    padding-right: var(--ui-space-2);
  }
}

/* Character places (map-places pack): the People panel that closes the
   building and marker editors, and — since container pack 2 — the
   places list on a character's card, which used to hand-copy it. A
   person's row is the annotation row without the click — the name here
   is who this place belongs to, not a way to reach something else. */
.place-people {
  margin-top: var(--ui-space-3);
  border-top: 1px solid var(--ui-edge);
  padding-top: var(--ui-space-2);
}

.place-people h3 {
  margin: 0 0 var(--ui-space-2);
  color: var(--ui-ink-muted);
}

.place-people h3 .count {
  margin-left: var(--ui-space-2);
  font-size: var(--ui-text-small);
  opacity: 0.7;
}

.place-people ul {
  list-style: none;
  margin: 0 0 var(--ui-space-2);
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--ui-space-1);
}

.person-row {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--ui-space-2);
}

.person-row .name {
  min-width: 8rem;
}

.person-row .meta {
  color: var(--ui-ink-muted);
  font-size: var(--ui-text-small);
}

.person-row .note {
  flex: 1;
  min-width: 8rem;
  font-style: italic;
}

.person-row .delete {
  margin-left: auto;
}

.place-people .control-row {
  margin-bottom: 0;
}

/* The store panel (stores pack A): between People and Notes on the
   building and marker editors, the same shape as both. A line row is
   read-only with Remove; the catch-all is the offer the store makes
   without a listed line.

   The For-sale panel (stores pack D) is its twin, mounted beside it
   because a place can be both a shop and for sale — so it wears these
   rules rather than a hand-copied set of its own. Only its one
   read-out line and the price beside its heading are its own, at the
   foot of this block. */
.place-store,
.place-listing {
  margin-top: 0.8rem;
  border-top: 1px solid var(--ui-edge);
  padding-top: 0.5rem;
}

/* Size, small-caps and tracking come from .block-title now; what is
   left is this block's own margin and ink. Its muted ink used to name
   the map's retired inline namespace and so resolved to nothing,
   leaving the heading to take its colour from inheritance; container
   pack 6 swept the last five of those reads onto the tokens they were
   always meant to be, this one among them. */
.place-store h3,
.place-listing h3 {
  margin: 0 0 0.4rem;
  color: var(--ui-ink-muted);
}

.place-store h3 .count,
.place-listing h3 .count {
  margin-left: 0.4rem;
  font-size: var(--ui-text-small);
  opacity: 0.7;
}

.place-store ul {
  list-style: none;
  margin: 0 0 0.5rem;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 0.3rem;
}

.place-store .store-head .delete {
  margin-left: auto;
}

.place-store .catch-all {
  margin: 0 0 0.5rem;
  font-size: var(--ui-text-small);
}

.place-store .catch-all p {
  margin: 0 0 0.3rem;
  color: var(--ui-ink-muted);
  display: flex;
  align-items: baseline;
  gap: 0.6rem;
}

/* Caption and not small: the toggle was deliberately a step under the
   catch-all text it sits in, and the scale's step below small is
   caption. */
.place-store .catch-all .toggle {
  font-size: var(--ui-text-caption);
  padding: 0.1rem 0.5rem;
}

.place-store .catch-all ul {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(14rem, 1fr));
  gap: 0.1rem 1rem;
}

.catch-row,
.line-row {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.6rem;
}

.line-row .name {
  min-width: 8rem;
}

.catch-row .meta,
.line-row .meta {
  color: var(--ui-ink-muted);
  font-size: var(--ui-text-small);
  white-space: nowrap;
}

.catch-row .meta {
  margin-left: auto;
}

.line-row .delete {
  margin-left: auto;
}

.place-store .control-row,
.place-listing .control-row {
  margin-bottom: 0;
}

/* Small, the nearest step down: it is a sub-heading under the store's
   own .block-title, so it must stay below body size and above nothing —
   its rank against the title above it is what the walkthrough checks. */
.place-store .buy-head,
.place-listing .buy-head {
  margin: 0.6rem 0 0.2rem;
  font-size: var(--ui-text-small);
  font-variant: small-caps;
  letter-spacing: 0.06em;
  color: var(--ui-ink-muted);
}

.place-store .buy-row .status {
  flex-basis: 100%;
}

/* What the listing is, at a glance, above its controls — the same
   dotted line the map row reads. */
.place-listing .listing-line {
  margin: 0 0 0.4rem;
  color: var(--ui-ink-muted);
  font-size: var(--ui-text-small);
}

.place-listing .control-row .delete {
  margin-left: auto;
}

/* The sale's verdict sits outside the Buy row, because the sale is
   what takes that row away: it has to outlive the listing it reports
   on (D6). */
.place-listing > .status {
  margin: 0.4rem 0 0;
}

/* Places on a character's card (map-places pack): the entity side of
   the People panel, and now literally the same block — it wears
   .place-people. It was hand-copied because those rules resolved the
   map's own inline colour namespace, set on the map section and
   undefined everywhere else; container pack 2 made them --ui-* on the
   root, so the copy has no reason left. Only the row differs. */
.place-row {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--ui-space-2);
}

.place-row .name {
  min-width: 10rem;
}

/* A ref whose building or marker is not on the map right now. */
.place-row .name.missing {
  color: var(--ui-ink-muted);
  font-style: italic;
}

.place-row .meta {
  color: var(--ui-ink-muted);
  font-size: var(--ui-text-small);
}

.place-row .note {
  flex: 1;
  min-width: 8rem;
  font-style: italic;
}

.place-row .show,
.place-row .delete {
  margin-left: auto;
}

/* Places of… (map-places decision 7): a view filter, among the view
   controls rather than among the tools since container pack 3. */
.view-menu .places-of > span {
  color: var(--ui-ink-muted);
}

.view-menu .places-of {
  margin: 0;
}

/* UI scale (container pack 3): the four steps side by side under their
   label, in the menu's own button idiom. What they write is the root
   element's font size, so everything measured in rem — this menu
   included — moves with them; the canvas box does not change. */
/* Render distance (render-distance item 3) is the same step group, under
   the scale. */
.view-menu .ui-scale > span,
.view-menu .render-distance > span {
  color: var(--ui-ink-muted);
}

.view-menu .ui-scale,
.view-menu .render-distance {
  margin: 0;
}

.scale-steps {
  display: flex;
  gap: var(--ui-space-1);
}

/* The base button's 0.75rem sides would not let four steps fit the 14rem
   View menu at any UI scale. */
.scale-steps button {
  flex: 1;
  font-size: var(--ui-text-small);
  padding-inline: var(--ui-space-1);
}

/* Notes (map-notes item 3): the world's notebook under the World tab. */
.notes-section {
  margin-top: var(--ui-space-3);
  border-top: 1px solid var(--ui-edge);
  padding-top: var(--ui-space-2);
}

.notes-section .section-head {
  display: flex;
  align-items: baseline;
  gap: var(--ui-space-3);
}

.notes-section h2 {
  margin: 0;
}

.notes-section h2 .count {
  margin-left: var(--ui-space-2);
  font-size: var(--ui-text-small);
  opacity: 0.7;
}

.note-list {
  list-style: none;
  margin: var(--ui-space-2) 0 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--ui-space-2);
}

.note-card {
  border: 1px solid var(--ui-edge);
  border-radius: var(--ui-radius-lg);
  padding: var(--ui-space-3) var(--ui-space-4);
}

.note-head {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: var(--ui-space-2);
}

.note-head .field.name {
  flex: 1;
  min-width: 12rem;
}

.note-head .date {
  color: var(--ui-ink-muted);
  font-size: var(--ui-text-small);
}

.note-head .delete {
  margin-left: auto;
}

/* Where a note sits: one chip per place, each a jump to the map. */
.note-places {
  list-style: none;
  margin: var(--ui-space-2) 0 0;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  gap: var(--ui-space-2);
  align-items: baseline;
}

.note-places .meta {
  color: var(--ui-ink-muted);
  font-size: var(--ui-text-small);
}

.note-places .chip {
  font-size: var(--ui-text-small);
  padding: var(--ui-space-1) var(--ui-space-2);
  /* A pill: any number past half the height does the same job. */
  border-radius: 999px;
}

/* A place that is not on the map right now: named, not offered. */
.note-places .chip.missing {
  font-style: italic;
  opacity: 0.7;
}

/* The standard clip idiom: the 1px box and its -1px margin are the
   recipe, not a measure. */
.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* The same notes, under the editor of one place (map-notes item 4). */
.place-notes {
  margin-top: var(--ui-space-3);
  border-top: 1px solid var(--ui-edge);
  padding-top: var(--ui-space-2);
}

.place-notes h3 {
  margin: 0 0 var(--ui-space-2);
  color: var(--ui-ink-muted);
}

.place-notes h3 .count {
  margin-left: var(--ui-space-2);
  font-size: var(--ui-text-small);
  opacity: 0.7;
}

/* Unlink sits away from Delete: two verbs, and only one is final. */
.note-foot {
  display: flex;
  align-items: baseline;
  gap: var(--ui-space-2);
  margin-top: var(--ui-space-1);
}

.note-foot .meta {
  color: var(--ui-ink-muted);
  font-size: var(--ui-text-small);
}

.note-foot .unlink {
  margin-left: auto;
}

.note-foot .check {
  display: inline-flex;
  align-items: center;
  gap: var(--ui-space-1);
  font-size: var(--ui-text-small);
}

/* An entity's notes, on its card (player-view D): closed by default as
   the sheet is, and the page among them marked by its edge. */
.entity-notes {
  margin-top: var(--ui-space-3);
  border-top: 1px solid var(--ui-edge);
  padding-top: var(--ui-space-2);
}

.entity-notes > .meta {
  color: var(--ui-ink-muted);
  font-size: var(--ui-text-small);
}

.entity-notes .mods-toggle .count {
  margin-left: var(--ui-space-1);
  opacity: 0.7;
}

.note-card.page {
  border-color: var(--ui-accent);
}

/* The Notes panel's entity chips are names, not jumps: no button. */
.note-entities .chip {
  border: 1px solid var(--ui-edge);
}

.note-entities .chip.page {
  border-color: var(--ui-accent);
}

/* Seats: who is at the table, under the entity list they select from
   (player-characters pack A). Borrows the notes section's shape —
   same rule above, same head — so the World panel reads as one column
   of sections rather than three unrelated boxes. */
.seats-section {
  margin-top: var(--ui-space-3);
  border-top: 1px solid var(--ui-edge);
  padding-top: var(--ui-space-2);
}

.seats-section h2 {
  margin: 0;
}

.seats-section h2 .count {
  margin-left: var(--ui-space-2);
  font-size: var(--ui-text-small);
  opacity: 0.7;
}

.seat-new {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: var(--ui-space-2);
  margin-top: var(--ui-space-2);
}

.seat-new .field.name {
  flex: 1;
  min-width: 12rem;
}

.seat-list {
  list-style: none;
  margin: var(--ui-space-2) 0 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--ui-space-2);
}

.seat-card {
  border: 1px solid var(--ui-edge);
  border-radius: var(--ui-radius-lg);
  padding: var(--ui-space-3) var(--ui-space-4);
}

.seat-head {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: var(--ui-space-2);
}

.seat-head .field.name {
  flex: 1;
  min-width: 12rem;
}

/* The access switch: a real checkbox, so its label is its hit target
   and the browser's own focus ring is the one shown. `.field` is
   already a baseline row; a box and its word want centring instead. */
.seat-head .field.switch {
  align-items: center;
}

.seat-head .delete {
  margin-left: auto;
}

/* The swap menu. A fieldset because the radios are one choice, and the
   legend is what a screen reader reads before them.

   The Shown block under it (player-view B1-6) is the card's other
   fieldset and takes the same reset, stated once here so the two cannot
   drift: B1 shipped it with only a margin, and it drew the browser's
   default groove border and padding around a block that is otherwise
   styled like its neighbour (2026-09-23 walkthrough). */
.seat-controls,
.seat-shown {
  margin: var(--ui-space-2) 0 0;
  padding: 0;
  border: 0;
}

.seat-controls legend,
.seat-shown legend {
  padding: 0;
  color: var(--ui-ink-muted);
  font-size: var(--ui-text-small);
}

.seat-controls ul {
  list-style: none;
  margin: var(--ui-space-1) 0 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--ui-space-1);
}

.seat-control {
  display: flex;
  align-items: center;
  gap: var(--ui-space-2);
}

.seat-control label {
  display: flex;
  align-items: center;
  gap: var(--ui-space-2);
  flex: 1;
  min-width: 0;
}

/* The one being played, named by weight rather than by a colour of its
   own: the radio already carries the state. */
.seat-control.primary > label > span {
  color: var(--ui-caption);
  font-weight: 600;
}

.seat-control .release {
  margin-left: auto;
  font-size: var(--ui-text-small);
}

/* The rules palette (rules R1): `/`, type, read the rule.
   Fan's third tenant, and the first one that is a dialog rather than a
   drawer — so it is split the way the other two are, because that split
   is the primitive's rule and not a preference. This element is the
   *frame*: where it is, how wide it is, which layer it is on, and
   nothing that can move it. Everything that paints and slides is the
   .fan-panel inside it.

   It is the first claimant of --ui-z-dialog. Above --ui-z-menu, because
   the palette opens over the dock and the View menu and is not one of
   the three surfaces decision 2 keeps mutually exclusive — it is asked
   for mid-anything, which is the point of a key that works everywhere.

   It still hangs below the save banner, by the banner's measured height
   (--save-banner-h, 0px while there is none): the banner sits inside the
   bar's stacking context, so at the bar's layer, and the palette's card
   painted over its bottom 12px (follow-ups sweep, join review).

   Nothing here invents a colour. The card reads at --ui-text-lead in
   the prose face, which is the table-legible size: this is the one
   surface in the app somebody reads out loud. */
.rules-palette {
  position: fixed;
  top: calc(var(--ui-chrome-top) + var(--ui-space-6) + var(--save-banner-h));
  /* Centred with auto margins and not with translateX(-50%): the fan
     primitive's root is the box anything measuring a fan reads, so it
     never carries a transform, and a transformed root would also become
     the containing block for any fixed descendant. Nothing measures
     this one today; the rule is the primitive's, not this tenant's. */
  left: 0;
  right: 0;
  margin-inline: auto;
  width: min(46rem, calc(100vw - var(--ui-space-8)));
  max-height: calc(100vh - var(--ui-chrome-top) - var(--ui-space-12) - var(--save-banner-h));
  z-index: var(--ui-z-dialog);
  display: flex;
}

/* A dialog hangs in the air like the View menu does, so it takes the
   same card treatment; the column, the gap, the padding, the surface
   and the slide all come from .fan-panel. */
.rules-palette .fan-panel {
  border: 1px solid var(--ui-edge);
  border-radius: var(--ui-radius-lg);
  box-shadow: var(--ui-elevation-2);
  background: var(--ui-surface-raised);
}

/* Which way it comes in. The from-state belongs to the tenant class,
   not to .fan-panel — see the motion block above — and a dialog drops
   from where it is rather than sliding from an edge. */
@starting-style {
  .rules-palette .fan-panel {
    transform: translateY(-0.75rem);
  }
}

.rules-find[hidden] {
  display: none;
}

.rules-find {
  display: flex;
  flex-direction: column;
  gap: var(--ui-space-2);
  min-height: 0;
}

.rules-find input[type="search"] {
  font-size: var(--ui-text-lead);
  padding: var(--ui-space-2);
}

/* The system chips. A filter that can hide everything needs a way back,
   so All is first and always there. */
.rules-systems {
  display: flex;
  flex-wrap: wrap;
  gap: var(--ui-space-1);
}

.rules-systems button[aria-pressed="true"] {
  background: var(--ui-accent);
  border-color: var(--ui-accent);
}

.rules-systems .count,
.command-group .count,
.command-section .count {
  margin-left: var(--ui-space-1);
  color: var(--ui-ink-muted);
  font-size: var(--ui-text-caption);
}

.rules-notice {
  margin: 0;
  color: var(--ui-warning);
  font-size: var(--ui-text-small);
}

.command-empty {
  margin: var(--ui-space-4) 0;
  color: var(--ui-ink-muted);
}

/* The list scrolls, the search row does not: typing must never push the
   field off its own palette. The command palette's list (command.rs):
   a heading a group (Tabs, Rules), a sub-heading a section inside one
   (a rule's system), a button a row. */
.command-hits {
  overflow: auto;
  min-height: 0;
}

.command-hits ul {
  list-style: none;
  margin: 0 0 var(--ui-space-2) 0;
  padding: 0;
}

/* A group's heading reads in the ink and a section's in the muted ink,
   so the two levels are told apart by weight rather than by a new
   colour. */
.command-group {
  margin: var(--ui-space-3) 0 var(--ui-space-1) 0;
  color: var(--ui-ink);
  text-transform: uppercase;
  font-size: var(--ui-text-small);
  letter-spacing: 0.12em;
}

.command-group:first-child {
  margin-top: 0;
}

.command-section {
  margin: var(--ui-space-2) 0 var(--ui-space-1) 0;
  color: var(--ui-ink-muted);
  text-transform: uppercase;
  font-size: var(--ui-text-caption);
  letter-spacing: 0.12em;
}

/* A row is a button so that the mouse and the keyboard reach the same
   act, and so that a row is a tab stop when somebody prefers Tab to the
   arrows. */
.command-row button {
  display: flex;
  align-items: baseline;
  gap: var(--ui-space-2);
  width: 100%;
  text-align: left;
  border-color: transparent;
  background: none;
  font-size: var(--ui-text-body);
}

.command-row.selected button,
.command-row button:hover {
  border-color: var(--ui-edge);
  background: var(--ui-surface);
}

.command-row.selected button {
  border-color: var(--ui-select);
}

.command-row .id,
.rules-card .id {
  color: var(--ui-ink-muted);
  font-family: var(--ui-font-data);
  font-size: var(--ui-text-small);
}

.command-row .title {
  flex: 1;
  min-width: 0;
}

/* Why a row is here — the tag that matched, when it is not the title.
   A hit on a word that is nowhere in the title is the one a reader
   would otherwise have to guess at. */
.command-row .why {
  color: var(--ui-ink-muted);
  font-size: var(--ui-text-caption);
}

.command-row .flag,
.rules-card .rules-override {
  color: var(--ui-warning);
  font-size: var(--ui-text-caption);
}

/* The card: one rule, at the size somebody reads out loud. */
.rules-card {
  display: flex;
  flex-direction: column;
  gap: var(--ui-space-2);
  overflow: auto;
  min-height: 0;
  font-family: var(--ui-font-prose);
}

.rules-card-head {
  display: flex;
  align-items: baseline;
  gap: var(--ui-space-2);
}

.rules-card-head .system {
  margin-left: auto;
  color: var(--ui-ink-muted);
  font-size: var(--ui-text-caption);
}

.rules-card h4 {
  display: flex;
  align-items: baseline;
  gap: var(--ui-space-2);
  margin: 0;
  font-size: var(--ui-text-title);
}

.rules-card h4:focus-visible {
  outline: 2px solid var(--ui-select);
  outline-offset: 2px;
}

.rules-card .rules-override {
  margin: 0;
}

.rules-roll {
  display: flex;
  align-items: baseline;
  gap: var(--ui-space-2);
  margin: 0;
}

.rules-roll .line {
  font-family: var(--ui-font-data);
  font-size: var(--ui-text-lead);
  color: var(--ui-select);
}

/* What the dialect made of that line. It is here for whoever is writing
   the files: a roll line the parser misread looks fine until you see
   what it became. */
.rules-terms {
  margin: 0;
  color: var(--ui-ink-muted);
  font-family: var(--ui-font-data);
  font-size: var(--ui-text-caption);
}

/* The body is pre-formatted and never markup. The format's indented
   blocks are content, and the app has no HTML sink — rendering
   GM-authored markup would be one. */
.rules-body {
  margin: 0;
  white-space: pre-wrap;
  font-family: var(--ui-font-prose);
  font-size: var(--ui-text-lead);
  line-height: 1.5;
}

.rules-see,
.rules-roll .label,
.rules-see .label {
  margin: 0;
}

.rules-see {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--ui-space-2);
}

.rules-roll .label,
.rules-see .label {
  color: var(--ui-ink-muted);
  text-transform: uppercase;
  font-size: var(--ui-text-small);
  letter-spacing: 0.12em;
}

/* A see: id this library does not have stays visible as text rather
   than being dropped — an author looking at a dead link is the point. */
.rules-see .missing {
  color: var(--ui-ink-muted);
  text-decoration: line-through;
}

.rules-card .link {
  border-color: transparent;
  background: none;
  padding: 0;
  color: var(--ui-accent);
  text-decoration: underline;
  font-family: var(--ui-font-prose);
  font-size: inherit;
}

.rules-source {
  margin: 0;
  color: var(--ui-ink-muted);
  font-size: var(--ui-text-caption);
}

/* --- The roll bar (rules R2) ---------------------------------------
   The check as slots somebody fills, and the dice that come out. It
   composes against the --ui-* layer like everything else: no literal
   colour, no literal spacing, and the one type scale.

   The shape says what the feature says. Candidates are buttons, not a
   dropdown, because a rule's two or three options should be one click;
   "More options…" is the same control widened to the whole vocabulary,
   and the free-text box beside it is always there — a suggested pool is
   never a gate, so nothing here may make an unlisted choice
   impossible. */
.roll-bar {
  display: flex;
  flex-direction: column;
  gap: var(--ui-space-2);
  padding: var(--ui-space-3);
  border: 1px solid var(--ui-edge);
  border-radius: var(--ui-radius-md);
  background: var(--ui-surface-raised);
}

.roll-head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--ui-space-2);
}

.roll-bar .label,
.roll-bar .field-row > span,
.roll-bar .slot-label {
  color: var(--ui-ink-muted);
  text-transform: uppercase;
  font-size: var(--ui-text-small);
  letter-spacing: 0.12em;
}

.roll-bar .field-row,
.roll-bar .check {
  display: inline-flex;
  align-items: baseline;
  gap: var(--ui-space-2);
  margin: 0;
}

.roll-bar .num {
  width: 4rem;
}

.roll-row,
.roll-slot,
.roll-more,
.roll-actions,
.roll-profiles,
.compose-slot,
.compose-add {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--ui-space-2);
}

.roll-slots {
  display: flex;
  flex-direction: column;
  gap: var(--ui-space-2);
}

.roll-slot .candidates {
  display: flex;
  flex-wrap: wrap;
  gap: var(--ui-space-1);
}

/* The chosen candidate, in the selection colour the palette's rows
   already use for "this is the one". */
.roll-slot .candidates button[aria-pressed="true"] {
  border-color: var(--ui-select);
  color: var(--ui-select);
}

/* "More options…" is deliberately quieter than the rule's own
   candidates: it is the door to everything, not a fourth suggestion. */
.roll-slot .candidates .more {
  border-style: dashed;
  border-color: var(--ui-edge);
  color: var(--ui-ink-muted);
}

.roll-more {
  width: 100%;
  padding-left: var(--ui-space-3);
  border-left: 2px solid var(--ui-edge);
}

.roll-note,
.roll-asked,
.compose-head {
  margin: 0;
  color: var(--ui-ink-muted);
  font-size: var(--ui-text-caption);
}

/* The GM's one line is the only thing on the bar written by a person,
   so it reads like prose and not like a control. */
.roll-asked {
  color: var(--ui-ink);
  font-family: var(--ui-font-prose);
  font-size: var(--ui-text-body);
  font-style: italic;
}

.roll-flat,
.roll-stated {
  color: var(--ui-ink-muted);
  font-family: var(--ui-font-data);
  font-size: var(--ui-text-caption);
}

.roll-actions button.primary {
  border-color: var(--ui-accent);
  color: var(--ui-ink);
}

.roll-actions kbd {
  margin-left: var(--ui-space-2);
  color: var(--ui-ink-muted);
  font-family: var(--ui-font-data);
  font-size: var(--ui-text-small);
}

.roll-actions button:disabled {
  opacity: 0.5;
  cursor: default;
}

.roll-compose {
  display: flex;
  flex-direction: column;
  gap: var(--ui-space-2);
  padding: var(--ui-space-3);
  border: 1px dashed var(--ui-edge);
  border-radius: var(--ui-radius-md);
}

.roll-profiles .profile-name {
  width: 8rem;
}

/* The result. The dice are the loud part — they are what a table looks
   at — and the verdict is beside the count rather than above it,
   because on an empty difficulty there is no verdict at all and the
   line must not look like it lost something. */
.roll-result {
  display: flex;
  flex-direction: column;
  gap: var(--ui-space-1);
  padding-top: var(--ui-space-2);
  border-top: 1px solid var(--ui-edge);
}

.roll-result .dice {
  margin: 0;
  color: var(--ui-select);
  font-family: var(--ui-font-data);
  font-size: var(--ui-text-lead);
  letter-spacing: 0.08em;
}

.roll-result .counts {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--ui-space-3);
  margin: 0;
}

.roll-result .successes {
  font-size: var(--ui-text-body);
}

.roll-result .outcome {
  font-variant: small-caps;
  letter-spacing: 0.06em;
}

.roll-result .pool,
.roll-result .margin,
.roll-result .custom {
  color: var(--ui-ink-muted);
  font-size: var(--ui-text-caption);
}

/* "custom pool" is information and never a warning: same muted ink as
   the pool size, no state colour, no icon. */
.roll-result .custom {
  font-style: italic;
}

/* Reduced motion. The duration tokens go to zero so anything the
   container builds on them stops moving too, and the one animation that
   cannot be stopped that way — an infinite spinner does not end because
   its duration is zero — is turned off by name. The pending way keeps
   its ring, just still. */
@media (prefers-reduced-motion: reduce) {
  :root {
    --ui-duration-fast: 0ms;
    --ui-duration-base: 0ms;
    --ui-duration-spin: 0ms;
  }

  .travel-menu .way.pending::after {
    animation: none;
  }
}

/* ---------------------------------------------------------------
   The play surface (player-view pack P): what a seat gets at
   /seat/<id>, and the ruler drawn over it. Appended as one block per
   the pack's dispatch rule — this stream edits no line it did not add
   here, because two streams splicing this file has already cost a
   commit (5cf5335).

   Almost nothing here is new. The bar takes .topbar's geometry and the
   map takes .map-stage and .map-canvas as they are, which is what "one
   shell, two audiences" means in CSS; what follows is the little that
   is this surface's own.
   --------------------------------------------------------------- */

/* The surface carries no layout of its own — its children are each
   fixed, exactly as the GM shell's are — so this undoes the reading
   column `main` carries by default, the same three declarations and
   for the same reason .shell does.

   **Not `display: contents`**, which would be the tidier way to say
   "this box is not a box" and would drop a landmark: `<main>` is how a
   screen reader finds the page, and a box with no geometry still has
   to be in the tree. */
.player-shell {
  max-width: none;
  margin: 0;
  padding: 0;
}

/* Which seat this page is, beside the wordmark. Quiet: a label a player
   checks once, not something they act on — so on this bar it, and not
   the wordmark, is what gives first: a long seat id ends in an ellipsis
   while "Play" keeps its width. Short of room for both, they go in
   turn, the seat line and then "Play", each going whole rather than
   the bar scrolling at 375px (the fold's phone rules, in [C1-seat]);
   this rule keeps "Play" from ever ending in an ellipsis itself. */
.player-bar h1 {
  flex-shrink: 0;
}

.player-seat {
  flex-shrink: 1;
  min-width: 0;
  margin: 0;
  font-size: var(--ui-text-small);
  color: var(--ui-ink-muted);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* The tools fan. A menu rather than a wall, so it takes the .view-menu
   treatment: this element is the *frame* — where it is, how wide it is,
   which layer it is on — and never moves, because it is the box
   anything measuring a fan reads. Everything that paints and slides is
   the .fan-panel inside it. */
.player-fan {
  position: fixed;
  top: var(--ui-chrome-top);
  right: var(--ui-space-3);
  width: 16rem;
  z-index: var(--ui-z-menu);
  display: flex;
}

.player-fan .fan-panel {
  border: 1px solid var(--ui-edge);
  border-radius: var(--ui-radius-lg);
  box-shadow: var(--ui-elevation-2);
}

/* The edge this fan comes in from, and it has to be its own block: a
   fan mounts on open, so it enters the document already at its resting
   style and a transition declared beside the resting rules has nothing
   to run from. Nothing fails without this — the fan simply appears. */
@starting-style {
  .player-fan .fan-panel {
    transform: translateX(100%);
  }
}

.player-fan button[aria-pressed="true"] {
  background: var(--ui-accent);
  border-color: var(--ui-accent);
}

/* The surface's one live region, under the bar and clear of MapLibre's
   own controls. Always mounted and usually silent. On the chrome's
   layer since player-character-sheet PC2: the Characters tab was then a
   layer at the scrim's over the map, and "Reconnecting…" under it
   would have been a page saying nothing while what it showed went
   stale. */
.player-status {
  position: fixed;
  top: calc(var(--ui-chrome-top) + var(--ui-space-3));
  left: var(--ui-space-3);
  z-index: var(--ui-z-chrome);
  max-width: min(28rem, calc(100vw - 2 * var(--ui-space-3)));
  pointer-events: none;
}

/* The ruler's controls, in the fan. The readout sits between the two
   buttons because it is what both of them are about. */
.ruler-panel {
  display: flex;
  flex-direction: column;
  gap: var(--ui-space-2);
  align-items: stretch;
}

.ruler-readout {
  margin: 0;
  font-size: var(--ui-text-small);
  color: var(--ui-ink-muted);
  font-variant-numeric: tabular-nums;
}

/* The measurement drawn over the map.

   **It never takes a click.** The layer covers the whole stage, so
   without pointer-events: none it would swallow every click meant for
   the map — including the ones that add the ruler's own points, which
   is a tool that stops working the moment it is used once. The
   transform on .ruler-chip is a descendant's, which the stage's
   containing-block invariant explicitly allows. */
.ruler-layer {
  position: absolute;
  inset: 0;
  z-index: var(--ui-z-map-overlay);
  pointer-events: none;
}

.ruler-line {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  /* Corners at the very edge of the stage are still drawn whole. */
  overflow: visible;
}

.ruler-line polyline {
  fill: none;
  stroke: var(--ui-accent);
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
}

/* The corners. They are one <marker> reused at every vertex of the
   polyline rather than a circle per point, so a pan writes one
   attribute instead of rebuilding the drawing — see RulerOverlay. */
.ruler-line marker circle {
  fill: var(--ui-accent);
  stroke: var(--ui-surface);
  stroke-width: 1.5;
}

/* The running total, hanging off the last point clicked. */
.ruler-chip {
  position: absolute;
  margin: 0;
  transform: translate(-50%, -2rem);
  padding: var(--ui-space-1) var(--ui-space-2);
  border: 1px solid var(--ui-edge);
  border-radius: var(--ui-radius-lg);
  background: var(--ui-surface-raised);
  color: var(--ui-ink);
  box-shadow: var(--ui-elevation-2);
  font-size: var(--ui-text-small);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

/* ============================================================
   Pack B2 — view as… The one block this stream owns; appended,
   and no line above it is edited (dispatch note, 2026-09-21).
   ============================================================ */

/* View as…: the seat picker in the bar, and the way back out of a
   seat view. The chip and the button are only there while a seat is
   being looked through — the bar is the one surface on screen in every
   state, and a GM who cannot tell which view they are in will make a
   ruling from the wrong one. */
.view-as {
  display: flex;
  align-items: center;
  gap: var(--ui-space-2);
}

/* The label and its picker are one line, always. `.field` wraps by
   default (its narrow-width pass puts a control under its label rather
   than scrolling the page) and this field inherited that: in a crowded
   bar the picker was squeezed to 190px where it needs 198, the label
   wrapped above the <select>, and a bar of fixed height centred the
   two-line box so the label sat off the top of the window. No wrap,
   and — like everything else in the bar — no shrink. */
.view-as .field,
.view-as .leave-seat {
  flex-shrink: 0;
}

.view-as .field {
  flex-direction: row;
  flex-wrap: nowrap;
  align-items: center;
  gap: var(--ui-space-2);
  margin: 0;
}

.view-as .field span {
  white-space: nowrap;
  font-size: var(--ui-text-small);
  color: var(--ui-ink-muted);
}

.view-as select {
  max-width: 12rem;
}

/* Seated: the warning colour, on the chip, on the picker and on the
   bar itself. Not the error colour — nothing has gone wrong — and not
   the accent, which every armed toggle in the bar already wears. */
.seat-chip {
  margin: 0;
  padding: var(--ui-space-1) var(--ui-space-3);
  border: 1px solid var(--ui-warning);
  border-radius: 999px;
  color: var(--ui-warning);
  font-size: var(--ui-text-small);
  white-space: nowrap;
  /* The seated bar's one give (see .shell.seated .view-as): it takes a
     seat's name, which can be any length, and it is the one thing on
     that bar that is a sentence rather than a control. */
  flex: 0 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
}

.view-as.seated select {
  border-color: var(--ui-warning);
}

.shell.seated .topbar {
  border-bottom-color: var(--ui-warning);
}

/* A seat's page (B2-2): the same map stage, and what the seat has
   been shown beside it. The panel is a fixed right rail like the GM's
   dock — the shape the camera already knows how to stay clear of. */
.player-panel {
  position: fixed;
  top: var(--ui-chrome-top);
  right: 0;
  bottom: 0;
  width: var(--ui-dock-width);
  z-index: var(--ui-z-map-overlay);
  overflow: auto;
  padding: var(--ui-space-4);
  background: var(--ui-surface);
  border-left: 1px solid var(--ui-edge);
}

.player-panel h2 {
  font-size: var(--ui-text-lead);
}

.player-empty {
  margin: 0;
  color: var(--ui-ink-muted);
}

.player-list {
  list-style: none;
  margin: var(--ui-space-2) 0 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--ui-space-2);
}

.player-list li {
  border: 1px solid var(--ui-edge);
  border-radius: var(--ui-radius-lg);
  padding: var(--ui-space-2) var(--ui-space-3);
}

.player-list .name {
  display: block;
  font-weight: bold;
}

.player-list .kind,
.player-list .carried {
  display: block;
  font-size: var(--ui-text-small);
  color: var(--ui-ink-muted);
}

.player-list.notes .md {
  margin-top: var(--ui-space-1);
}

/* A note's body as markdown (player-view D, markdown.rs): the panel's
   notes and an NPC's page. A line break the GM typed stays one. The
   panel's own boxed rows are its list items, not the note's. */
.md > :first-child {
  margin-top: 0;
}

.md p,
.md ul,
.md ol,
.md blockquote,
.md pre,
.md hr {
  margin: var(--ui-space-2) 0 0;
}

.md p {
  white-space: pre-line;
}

.md h4,
.md h5,
.md h6 {
  margin: var(--ui-space-3) 0 0;
}

.md ul,
.md ol {
  padding-left: var(--ui-space-5);
}

.md ul {
  list-style: disc;
}

.md ol {
  list-style: decimal;
}

.player-list .md li {
  border: none;
  border-radius: 0;
  padding: 0;
}

.md blockquote {
  border-left: 2px solid var(--ui-edge);
  padding-left: var(--ui-space-2);
  color: var(--ui-ink-muted);
}

.md pre {
  overflow-x: auto;
  white-space: pre;
}

.md a {
  color: inherit;
  text-decoration: underline;
}

.md hr {
  border: none;
  border-top: 1px solid var(--ui-edge);
}

/* A Party row's page, and the fan it opens (seat_pages.rs), which wears
   the Messages fans' frame. */
.player-list .page-door {
  margin-top: var(--ui-space-1);
  font-size: var(--ui-text-small);
}

.seat-page {
  overflow-wrap: anywhere;
}

.seat-page h3 {
  margin: 0 0 var(--ui-space-2);
}

/* B2-5: with the bar-map controls absent, the picker is what holds
   the right end of the bar.

   **And "Back to GM view" is never what gives.** A GM who cannot get
   out of a seat's view is stuck making rulings from the wrong one,
   which is the worst way this bar can fail — so on a seat's page this
   group may shrink, and the only thing inside it that can is the chip:
   the label, the picker and the button keep their width, the chip's
   sentence ends in an ellipsis instead, and the button stays inside
   the bar and clear of the autosave banner fixed beneath it. */
.shell.seated .view-as {
  flex-shrink: 1;
  min-width: 0;
  margin-left: auto;
}

/* ---------------------------------------------------------------
   Reveal controls (player-view pack B1): the seat picker and the
   toggle that shows one object to one seat, plus the seat card's
   read-back of everything it has been shown.

   Appended as one block at the end of the file, and editing no line
   above it, because B1, B2 and P are three concurrent streams — the
   last time two of them edited this file at once it cost commit
   5cf5335, "repair style.css — the seats and rules blocks were
   spliced".

   Built from the existing .field / .meta / chip rules rather than new
   shapes: a reveal control is a small labelled select with a pressed
   toggle beside it, and it sits inside panels of every width, so it
   wraps instead of setting a width of its own.
   --------------------------------------------------------------- */

.reveal-control {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: var(--ui-space-2);
  margin-top: var(--ui-space-2);
}

.reveal-control .field {
  min-width: 9rem;
}

.reveal-control .field span {
  font-size: var(--ui-text-small);
  color: var(--ui-ink-muted);
}

/* The toggle reads as unpressed chrome until the grant is held, and
   then as the selection highlight — the same colour the map uses for
   "this is the one you picked", because that is what it means here
   too. */
.reveal-control .reveal {
  border-radius: var(--ui-radius-lg);
}

.reveal-control .reveal.shown {
  border-color: var(--ui-select);
  color: var(--ui-select);
}

.reveal-control .reveal:disabled {
  opacity: 0.5;
  cursor: default;
}

/* What a seat has been shown (B1-6), on the seat card: one group per
   collection, each grant a row that can be taken back from here. Its
   fieldset reset is .seat-controls' (above); this is only the room
   around it, which the browser's default padding used to supply — the
   card's "Add character" field follows it directly. */
.seat-shown {
  margin: var(--ui-space-3) 0 var(--ui-space-2);
}

/* The count, apart from its legend: text and element meet with no space
   between them in the view, so without this it read "Shown1". The same
   badge every other section count wears. */
.seat-shown legend .count {
  margin-left: var(--ui-space-2);
  font-size: var(--ui-text-small);
  opacity: 0.7;
}

.seat-shown ul {
  list-style: none;
  margin: var(--ui-space-1) 0 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--ui-space-1);
}

.seat-shown .shown-group {
  margin-top: var(--ui-space-2);
}

.seat-shown .shown-group > .meta {
  font-size: var(--ui-text-small);
  color: var(--ui-ink-muted);
}

.seat-shown .shown-row {
  display: flex;
  align-items: baseline;
  gap: var(--ui-space-2);
}

.seat-shown .shown-row .name {
  flex: 1;
  min-width: 0;
}

/* A grant whose object the world no longer holds: it is pruned on the
   next load, and until then it reads as gone rather than vanishing. */
.seat-shown .shown-row .name.missing {
  color: var(--ui-ink-muted);
  font-style: italic;
}

/* The line above a control whose grant is wider or narrower than the
   thing the panel is editing: a claim shows every untouched feature in
   it, and a feature has no grant of its own at all. Quiet, because it
   is a caveat and not a warning. */
.reveal-note {
  margin: var(--ui-space-3) 0 0;
  font-size: var(--ui-text-small);
}

/* ---- Player-view pack E: spotting on the move ----------------------
   The spot log under the token tray, the verdict it leads each row
   with, and the seat card's Spot pool field. Appended, per the
   concurrent-stream rule for this file. */
.spot-row .spot-verdict {
  flex: none;
  min-width: 4.5rem;
  padding: 0 var(--ui-space-2);
  border: 1px solid var(--ui-edge);
  border-radius: var(--ui-radius-sm);
  color: var(--ui-ink-muted);
  font-size: var(--ui-text-small);
  text-align: center;
}

/* A hit is the row that changed what a seat can see, so it is the one
   drawn in the accent; a miss stays quiet. */
.spot-row.hit .spot-verdict {
  border-color: var(--ui-accent);
  color: var(--ui-accent);
}

.spot-row .spot-who {
  font-weight: 600;
}

/* The pin names a place the GM can go to; the verdict, who and the dice
   wrap under it on a narrow dock rather than squeezing it. */
.spot-row .name {
  min-width: 6rem;
}

/* The label beside a short number box, like Name and Enabled beside it,
   rather than wrapped above a box too narrow to hold both. */
.seat-head .field.pool {
  flex: none;
  align-items: center;
}

.seat-head .field.pool input {
  width: 4.5rem;
}

/* ---------------------------------------------------------------
   Seats over the wire, the client (network-bind N2-6): the play
   surface says what its link did. Appended as one block; no line
   above is edited.
   --------------------------------------------------------------- */

/* The seat's own state — opening, a dead link, an unreadable campaign,
   a server that is not answering — as a card in the surface's one live
   region. Silent keeps its node and drops only its card, the status
   stack's rule, so the region is never re-created to speak. */
.player-status .seat-state {
  margin: 0 0 var(--ui-space-2);
  padding: var(--ui-space-2) var(--ui-space-3);
  border: 1px solid var(--ui-edge);
  border-radius: var(--ui-radius-lg);
  background: var(--ui-surface);
  color: var(--ui-ink);
}

.player-status .seat-state.error {
  border-color: var(--ui-error);
}

.player-status .seat-state.silent {
  margin: 0;
  padding: 0;
  border: 0;
  background: none;
}

/* The server's own words sit under this page's sentence, quieter, and
   never instead of it. */
.player-status .seat-state .words:not(:empty) {
  display: block;
  margin-top: var(--ui-space-1);
  color: var(--ui-ink-muted);
}

/* Once seated, the panel is a right-hand rail at the dock's width, and
   MapLibre's own controls — zoom and the attribution — step out from
   under it exactly as they do from the GM's open dock: it publishes the
   same property .shell.dock-open does, and they read it. */
.player-shell:has(.player-panel) {
  --ui-chrome-right: var(--ui-dock-width);
}

/* …and the GM's View as… (pack B2) draws the same panel over the GM's
   own shell, so it takes the same rule (network-bind N2 follow-up F6).
   Neither .dock-open nor .data-open is set while a seat is being looked
   through (shell.rs), so nothing else publishes the property here. */
.shell.seated:has(.player-panel) {
  --ui-chrome-right: var(--ui-dock-width);
}

/* ---------------------------------------------------------------
   Seats over the wire, the GM's side (network-bind N2-7): a seat's
   link on its card. Appended as one block; no line above is edited.
   --------------------------------------------------------------- */

/* The card's third fieldset, with the reset its two siblings share. */
.seat-links {
  margin: var(--ui-space-2) 0 0;
  padding: 0;
  border: 0;
}

.seat-links legend {
  padding: 0;
  color: var(--ui-ink-muted);
  font-size: var(--ui-text-small);
}

.seat-links ul {
  list-style: none;
  margin: var(--ui-space-1) 0 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--ui-space-1);
}

/* One row per address: where it points, the link itself read-only and
   taking the width, and Copy. */
.seat-link {
  display: flex;
  align-items: flex-end;
  gap: var(--ui-space-2);
}

.seat-link .field {
  flex: 1;
  min-width: 0;
}

.seat-link input {
  width: 100%;
  font-family: var(--ui-font-data);
  font-size: var(--ui-text-small);
}

.seat-links .meta {
  margin: var(--ui-space-1) 0 0;
  font-size: var(--ui-text-small);
  color: var(--ui-ink-muted);
}

.seat-links .meta.silent {
  display: none;
}

.seat-links .make-link {
  margin-top: var(--ui-space-1);
}

/* ---------------------------------------------------------------
   Live update, the client (network-bind N3-4): what the seat page
   lays over itself while it is live. Appended as one block; no line
   above is edited.
   --------------------------------------------------------------- */

/* "Reconnecting…", or the stream parked until the tab is next shown: a
   card in the same live region, under the state's own and never instead
   of it, because what the page drew stays drawn. Quieter than the
   state's card — not the error colour, since nothing the player did is
   wrong — and silent keeps its node, the status stack's rule. */
.player-status .seat-overlay {
  margin: 0 0 var(--ui-space-2);
  padding: var(--ui-space-1) var(--ui-space-3);
  border: 1px dashed var(--ui-edge);
  border-radius: var(--ui-radius-lg);
  background: var(--ui-surface);
  color: var(--ui-ink-muted);
  font-size: var(--ui-text-small);
}

/* Parked — another window has the seat, or the server has no room for
   another stream: a sentence to read, not a passing state, so it takes
   the ink and a solid edge. */
.player-status .seat-overlay.parked {
  border-style: solid;
  color: var(--ui-ink);
}

.player-status .seat-overlay.silent {
  margin: 0;
  padding: 0;
  border: 0;
  background: none;
}

/* Silent, a line of the status panel has no area at all: neither
   .status's min-height nor a paragraph's margins. Once one line shows,
   the panel is as wide as it, and a silent line with height there
   pushed the shown one down and dropped a narrow screen's open Clock
   (C9) below an empty box (review). */
.player-status .status.silent {
  min-height: 0;
  margin: 0;
}

/* The character sheet (player-character-sheet PC1): a collapsible block
   at the foot of a person's or an NPC's card, and the sheet itself —
   `CharacterSheet` in sheet_ui.rs, the component a seat's Character
   drawer mounts (a tab from PC2, a drawer since MV1). Its panels sit on the raised card in the page's own
   surface, which is the design's sunk panel without a slot of its own.
   Wounds take the state colours (superficial the warning, aggravated the
   error, as the canvas draws them) and Hunger takes --ui-blood. */
.entity-sheet {
  margin-top: var(--ui-space-3);
  border-top: 1px solid var(--ui-edge);
  padding-top: var(--ui-space-2);
}

.entity-sheet-bar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--ui-space-2) var(--ui-space-3);
}

.sheet-confirm {
  display: inline-flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--ui-space-2);
  color: var(--ui-error);
  font-size: var(--ui-text-small);
}

.sheet {
  display: flex;
  flex-direction: column;
  gap: var(--ui-space-3);
  margin-top: var(--ui-space-3);
}

.sheet-head {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--ui-space-2) var(--ui-space-4);
}

.sheet-identity {
  margin: 0;
  color: var(--ui-ink-muted);
  font-size: var(--ui-text-small);
}

.sheet-mode {
  display: inline-flex;
}

.sheet-mode button {
  border-radius: 0;
}

.sheet-mode button:first-child {
  border-radius: var(--ui-radius-md) 0 0 var(--ui-radius-md);
}

.sheet-mode button:last-child {
  border-left: 0;
  border-radius: 0 var(--ui-radius-md) var(--ui-radius-md) 0;
}

.sheet-mode button[aria-pressed="true"] {
  background: var(--ui-accent);
  border-color: var(--ui-accent);
}

.sheet-identity-edit {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--ui-space-2) var(--ui-space-4);
}

.sheet-identity-edit input[type="number"] {
  width: 5rem;
}

/* The tracker strip: four cards on a wide card, fewer a row as it
   narrows. Every sheet grid's minimum is capped at the grid's own
   width (PC2), so on a phone a column narrower than the minimum is one
   column and not a sideways scroll. */
.sheet-tracks {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(12rem, 100%), 1fr));
  gap: var(--ui-space-3);
}

.sheet-track,
.sheet-column,
.sheet-block {
  background: var(--ui-surface);
  border: 1px solid var(--ui-edge);
  border-radius: var(--ui-radius-lg);
  padding: var(--ui-space-2) var(--ui-space-3);
}

.sheet-track {
  display: flex;
  flex-direction: column;
  gap: var(--ui-space-2);
}

.sheet-track-head {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: var(--ui-space-2);
}

.sheet-track-label,
.sheet-column-title {
  margin: 0;
  color: var(--ui-ink-muted);
  font-family: var(--ui-font-data);
  font-size: var(--ui-text-caption);
  font-weight: 600;
  letter-spacing: 0.1em;
  text-transform: uppercase;
}

.sheet-track-readout {
  color: var(--ui-ink-muted);
  font-family: var(--ui-font-data);
  font-size: var(--ui-text-caption);
}

.sheet-track-text {
  color: var(--ui-ink);
}

.sheet-track-status {
  min-height: 1em;
  color: var(--ui-warning);
  font-size: var(--ui-text-caption);
}

.sheet-boxes {
  display: flex;
  flex-wrap: wrap;
  gap: var(--ui-space-1);
}

/* A box is a button with none of a button's padding: its mark is the
   whole of what it shows. */
.sheet-box {
  width: 1.5rem;
  height: 1.5rem;
  padding: 0;
  display: grid;
  place-items: center;
  background: none;
  border: 1px solid var(--ui-ink-muted);
  border-radius: var(--ui-radius-sm);
  font-family: var(--ui-font-data);
  font-size: var(--ui-text-small);
  line-height: 1;
}

.sheet-box.superficial {
  border-color: var(--ui-warning);
  color: var(--ui-warning);
}

.sheet-box.aggravated {
  border-color: var(--ui-error);
  color: var(--ui-error);
  background: color-mix(in srgb, var(--ui-error) 18%, transparent);
}

.sheet-box.sheet-hunger {
  border-radius: 50%;
  border-color: var(--ui-blood);
}

.sheet-box.sheet-hunger.filled {
  background: var(--ui-blood);
}

/* Ten boxes share one row however narrow the card: each starts from
   nothing and grows to its full width, so the tenth never wraps alone. */
.sheet-box.sheet-humanity {
  width: auto;
  flex: 1 1 0;
  min-width: 0.625rem;
  max-width: 1.125rem;
  color: var(--ui-warning);
}

.sheet-box.sheet-humanity.filled {
  background: var(--ui-ink);
  border-color: var(--ui-ink);
}

.sheet-stepper {
  display: inline-flex;
  align-items: center;
  gap: var(--ui-space-2);
  font-size: var(--ui-text-small);
}

.sheet-stepper-caption {
  color: var(--ui-ink-muted);
}

.sheet-stepper button {
  padding: 0 var(--ui-space-2);
}

.sheet-stepper button:disabled,
.sheet-add button:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

.sheet-stepper-value {
  min-width: 1.5em;
  text-align: center;
  font-family: var(--ui-font-data);
}

/* Attributes and skills: three columns on a wide card. */
.sheet-columns {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(15rem, 100%), 1fr));
  gap: var(--ui-space-3);
}

.sheet-column-title {
  margin-bottom: var(--ui-space-1);
}

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

.sheet-skills {
  margin-top: var(--ui-space-2);
  padding-top: var(--ui-space-2);
  border-top: 1px solid var(--ui-edge);
}

.sheet-trait {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 0 var(--ui-space-2);
  min-height: 1.75rem;
}

.sheet-trait.unrated .sheet-trait-name {
  color: var(--ui-ink-muted);
}

.sheet-specialty,
.sheet-note {
  margin-left: var(--ui-space-2);
  color: var(--ui-ink-muted);
  font-size: var(--ui-text-caption);
  font-style: italic;
}

.sheet-specialty-input,
.sheet-note-input {
  flex-basis: 100%;
  font-size: var(--ui-text-small);
}

/* The dot row, charpage's: a filled dot is ink, an empty one an outline,
   and in View the dots are disabled buttons that still read at full
   strength. */
.sheet .dots {
  display: inline-flex;
  gap: 0.125rem;
}

.sheet .dot {
  width: 1.125rem;
  height: 1.25rem;
  padding: 0;
  display: grid;
  place-items: center;
  background: none;
  border: 0;
}

.sheet .dot::before {
  content: "";
  width: 0.7rem;
  height: 0.7rem;
  border-radius: 50%;
  border: 1px solid var(--ui-ink-muted);
}

.sheet .dot.filled::before {
  background: var(--ui-ink);
  border-color: var(--ui-ink);
}

.sheet .dot:disabled {
  cursor: default;
}

.sheet-block-title {
  margin: 0 0 var(--ui-space-2);
  font-size: var(--ui-text-body);
  font-variant: small-caps;
  letter-spacing: 0.06em;
}

.sheet-empty {
  margin: 0;
  color: var(--ui-ink-muted);
  font-size: var(--ui-text-small);
}

.sheet-discipline-list,
.sheet-powers,
.sheet-advantage-list {
  list-style: none;
  margin: 0;
  padding: 0;
}

.sheet-discipline + .sheet-discipline {
  margin-top: var(--ui-space-2);
  padding-top: var(--ui-space-2);
  border-top: 1px solid var(--ui-edge);
}

.sheet-discipline-head,
.sheet-advantage-head {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--ui-space-1) var(--ui-space-3);
}

.sheet-discipline-head .sheet-trait-name,
.sheet-advantage-head .sheet-trait-name,
.sheet-advantage-head input[type="text"] {
  flex: 1;
  min-width: 8rem;
}

.sheet-powers {
  margin: var(--ui-space-1) 0 0 var(--ui-space-4);
}

.sheet-power {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--ui-space-1) var(--ui-space-2);
  padding: 0.125rem 0;
}

.sheet-advantage-groups {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(14rem, 100%), 1fr));
  gap: var(--ui-space-3);
}

.sheet-advantage {
  padding: var(--ui-space-1) 0;
}

.sheet-advantage + .sheet-advantage {
  border-top: 1px solid var(--ui-edge);
}

.sheet-add {
  margin: var(--ui-space-2) 0 0;
}

/* ---------------------------------------------------------------
   A seat's Characters tab (player-character-sheet PC2), since
   map-view drawers MV1 its Character drawer, on a seat's page and in
   the GM's View as… (the drawer's own rules are in [MV1]). Appended as
   one block. Two rules above are edited in place, each saying so:
   .player-status's layer and the sheet grids' minimum.
   --------------------------------------------------------------- */

/* The Character drawer's contents (map-view drawers MV1): the drawer's
   panel is its place, its width and its scroll ([MV1]), so the layer
   is one column inside it, the sheet's card, then the tray's bar and
   the line under them. */
.seat-characters {
  min-width: 0;
}

.seat-characters-column {
  display: flex;
  flex-direction: column;
  gap: var(--ui-space-3);
  min-width: 0;
}

/* The sheet on a raised card, its panels sunk into the page's surface
   as on the GM's card. Long text — a custom clan, a specialty, an
   advantage's note — wraps inside it rather than widening the page. */
.seat-sheet-card {
  min-width: 0;
  padding: var(--ui-space-4) var(--ui-space-5);
  background: var(--ui-surface-raised);
  border: 1px solid var(--ui-edge);
  border-radius: var(--ui-radius-lg);
  box-shadow: var(--ui-elevation-1);
  overflow-wrap: anywhere;
}

/* The line in place of a sheet — not a character, no sheet yet — and
   the seat that controls nothing. */
.seat-sheet-note {
  margin: var(--ui-space-2) 0 0;
  color: var(--ui-ink-muted);
}

.seat-characters-column > .seat-sheet-note {
  margin: 0;
}

/* A read-only sheet's boxes are read, never pressed: no pointer and no
   hover, so the border each mark sets is the one that shows. */
.sheet-box:disabled {
  cursor: default;
  pointer-events: none;
}

/* A box or a stains button whose change is still out (PC8):
   aria-disabled rather than disabled, so it keeps the focus it was
   pressed with, and dimmed as the stepper's disabled buttons are. The
   tray's ★ is dimmed the same way, in [MV1]. */
.sheet-box[aria-disabled="true"],
.sheet-stepper button[aria-disabled="true"] {
  opacity: 0.5;
  cursor: progress;
}

/* A proposal on the seat's sheet (PC7): sent and waiting for the GM, a
   wash of --ui-pending; still in the tray, a dashed rim of it. The word
   beside the row says which, so the paint is never the only sign. */
.sheet .pending {
  background: color-mix(in srgb, var(--ui-pending) 12%, transparent);
  outline: 1px solid color-mix(in srgb, var(--ui-pending) 55%, transparent);
  outline-offset: 1px;
}

.sheet .drafted {
  outline: 1px dashed var(--ui-pending);
  outline-offset: 1px;
}

.sheet-pending-tag {
  margin-left: var(--ui-space-2);
  color: var(--ui-pending);
  font-size: var(--ui-text-caption);
  letter-spacing: 0.08em;
  text-transform: uppercase;
}

/* The tray's bar under the seat's card (PC7): what waits to be sent,
   one line each, and Discard and Send to GM. */
.seat-tray {
  display: flex;
  flex-direction: column;
  gap: var(--ui-space-2);
  min-width: 0;
}

.seat-tray-count {
  margin: 0;
  color: var(--ui-pending);
  font-weight: 600;
}

.seat-tray-note {
  margin: 0;
  color: var(--ui-ink-muted);
  font-size: var(--ui-text-caption);
}

.seat-tray-list {
  margin: 0;
  padding-left: var(--ui-space-5);
  overflow-wrap: anywhere;
}

.seat-tray-list li.moved-on {
  color: var(--ui-warning);
}

.seat-tray-flag {
  margin-left: var(--ui-space-2);
  font-size: var(--ui-text-caption);
}

.seat-tray-actions {
  flex-wrap: wrap;
}

.seat-tray-actions button[aria-disabled="true"] {
  opacity: 0.5;
  cursor: progress;
}

/* A declined proposal: a line under the card until dismissed. */
.seat-declined {
  display: flex;
  flex-direction: column;
  gap: var(--ui-space-1);
  margin: var(--ui-space-2) 0 0;
  padding: 0;
  list-style: none;
}

.seat-declined:empty {
  display: none;
}

.seat-declined li {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--ui-space-2);
  margin: 0;
  overflow-wrap: anywhere;
}

/* [PC3 — rolling from the sheet (player-character-sheet PC3)] -------
   The region the sheet's pick, preview and roll own, reserved ahead of
   their code. PC3 appends its rules between this marker and the one
   that closes it, and edits no line outside them.
   --------------------------------------------------------------- */

/* A trait's name in View is a pick for a roll. It reads as the name it
   replaces — no button chrome, the unrated skill's muted ink — until it
   is hovered, focused or picked; picked, it is tinted and ringed in the
   selection gold, so the state is a shape as well as a colour. */
.sheet-pick {
  padding: 0 var(--ui-space-1);
  margin: 0 calc(-1 * var(--ui-space-1));
  border: 0;
  border-radius: var(--ui-radius-sm);
  background: none;
  color: inherit;
  font: inherit;
  text-align: start;
  text-decoration: underline dotted transparent;
  text-underline-offset: 0.2em;
}

.sheet-pick:hover {
  text-decoration-color: currentColor;
}

.sheet-pick[aria-pressed="true"] {
  color: var(--ui-ink);
  background: color-mix(in srgb, var(--ui-select) 20%, transparent);
  box-shadow: inset 0 0 0 1px var(--ui-select);
}

/* The roll panel, under the columns in View. At rest it is one muted
   line saying how to start. While something is picked it rides the
   bottom of whatever scrolls the sheet — a seat's Characters layer, the
   GM's panel — so the preview stays beside the columns it is picked
   from, and it sits raised over them. */
.sheet-roll {
  display: flex;
  flex-direction: column;
  gap: var(--ui-space-2);
  padding: var(--ui-space-2) var(--ui-space-3);
  background: var(--ui-surface);
  border: 1px dashed var(--ui-edge);
  border-radius: var(--ui-radius-lg);
}

.sheet-roll.picking {
  position: sticky;
  bottom: 0;
  z-index: 1;
  background: var(--ui-surface-raised);
  border-style: solid;
  box-shadow: var(--ui-elevation-2);
}

.sheet-roll-line {
  margin: 0;
  color: var(--ui-ink-muted);
  font-size: var(--ui-text-small);
}

.sheet-roll.picking .sheet-roll-line {
  color: var(--ui-ink);
}

/* One pip per die, regular first and Hunger last, as the engine draws
   them. A Hunger die is round and blood-red, the Hunger track's own
   mark, so it is told apart by its shape too. */
.sheet-roll-dice {
  display: flex;
  flex-wrap: wrap;
  gap: var(--ui-space-1);
}

.sheet-die {
  width: 0.875rem;
  height: 0.875rem;
  border-radius: var(--ui-radius-sm);
  background: var(--ui-ink);
}

.sheet-die.hunger {
  border-radius: 50%;
  background: var(--ui-blood);
}

.sheet-roll-actions {
  align-items: center;
  gap: var(--ui-space-2);
  margin: 0;
}

.sheet-roll-go {
  background: var(--ui-accent);
  border-color: var(--ui-accent);
}

.sheet-roll-go:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

/* The result, in the rules library's roll-result voice: the dice line
   large in the data face, the count under it. Before the first roll,
   and after each new pick, the block is clipped away by
   `.visually-hidden` rather than `hidden`, so its live region
   (`.sheet-roll-said`) stays in the tree to be heard when it fills;
   clipped, it is out of the flow and takes no gap. Its rule is drawn
   only while it shows, so the clip stays the idiom's 1px box. */
.sheet-roll-result,
.sheet-roll-said {
  display: flex;
  flex-direction: column;
  gap: var(--ui-space-1);
}

.sheet-roll-result:not(.visually-hidden) {
  padding-top: var(--ui-space-2);
  border-top: 1px solid var(--ui-edge);
}

.sheet-roll-result p {
  margin: 0;
}

.sheet-roll-heading,
.sheet-roll-note {
  color: var(--ui-ink-muted);
  font-size: var(--ui-text-caption);
}

.sheet-roll-dice-line {
  color: var(--ui-select);
  font-family: var(--ui-font-data);
  font-size: var(--ui-text-lead);
  letter-spacing: 0.08em;
  overflow-wrap: anywhere;
}

.sheet-roll-count {
  font-weight: 600;
}

/* The phone (PC3-4, read at 375 px): a name is a finger's width tall,
   the panel's line reads at body size, the two buttons split the
   panel's width, and the panel clears a home indicator. */
@media (max-width: 640px) {
  .sheet-pick {
    min-height: 2.25rem;
  }

  .sheet-roll {
    padding-bottom: max(var(--ui-space-2), env(safe-area-inset-bottom));
  }

  .sheet-roll-line {
    font-size: inherit;
  }

  .sheet-roll-actions button {
    flex: 1 1 0;
    min-height: 2.75rem;
  }
}

/* [/PC3] ---------------------------------------------------------- */

/* [PC4 — a seat's Inventory tab (player-character-sheet PC4)] -------
   The region the Loadout owns — Equipped, the containers under On you
   and each place, the space and pocket bars, the carry card — reserved
   ahead of its code. PC4 appends its rules between this marker and the
   one that closes it, and edits no line outside them.
   --------------------------------------------------------------- */

/* The Inventory drawer's contents (map-view drawers MV1): the drawer's
   panel is its place, its width and its scroll ([MV1]), so the layer
   is one column inside it — the switcher, the name, then the Loadout,
   laid out narrow below. */
.seat-inventory {
  min-width: 0;
}

.seat-inventory-column {
  display: flex;
  flex-direction: column;
  gap: var(--ui-space-3);
  min-width: 0;
}

.seat-inventory-column > .seat-sheet-note {
  margin: 0;
}

.seat-inventory-name {
  margin: 0;
  font-size: var(--ui-text-display);
  overflow-wrap: anywhere;
}

/* The canvas's two columns: Equipped and the carry card beside the
   containers, which take the rest. One column on a phone (below). */
.seat-loadout {
  display: grid;
  grid-template-columns: minmax(15rem, 22rem) minmax(0, 1fr);
  gap: var(--ui-space-4);
  align-items: start;
}

.seat-loadout-side {
  display: flex;
  flex-direction: column;
  gap: var(--ui-space-4);
  min-width: 0;
}

/* A panel on a raised card, as the sheet's is; long text wraps inside
   it rather than widening the page. */
.loadout-panel {
  min-width: 0;
  padding: var(--ui-space-3) var(--ui-space-4);
  background: var(--ui-surface-raised);
  border: 1px solid var(--ui-edge);
  border-radius: var(--ui-radius-lg);
  box-shadow: var(--ui-elevation-1);
  overflow-wrap: anywhere;
}

.loadout-panel-head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--ui-space-1) var(--ui-space-2);
  margin-bottom: var(--ui-space-2);
}

/* The canvas's small caps: a panel's name and a group's. */
.loadout-panel-head h3,
.loadout-group-head {
  margin: 0;
  color: var(--ui-ink-muted);
  font-size: var(--ui-text-caption);
  font-weight: 600;
  letter-spacing: 0.1em;
  text-transform: uppercase;
}

.loadout-panel-note {
  color: var(--ui-ink-muted);
  font-size: var(--ui-text-caption);
}

.loadout-slots,
.loadout-slot-things,
.loadout-items {
  margin: 0;
  padding: 0;
  list-style: none;
}

/* Equipped, as a list: each slot in the selection gold, what is worn
   there beside it. */
.loadout-slots {
  display: flex;
  flex-direction: column;
  gap: var(--ui-space-2);
}

.loadout-slot {
  display: grid;
  grid-template-columns: minmax(4.5rem, 7rem) minmax(0, 1fr);
  gap: var(--ui-space-2);
  align-items: baseline;
  padding-top: var(--ui-space-2);
  border-top: 1px solid var(--ui-edge);
}

.loadout-slot:first-child {
  padding-top: 0;
  border-top: 0;
}

.loadout-slot-name {
  color: var(--ui-select);
  font-size: var(--ui-text-small);
  font-weight: 600;
}

.loadout-slot-things {
  display: flex;
  flex-direction: column;
  gap: var(--ui-space-1);
}

.loadout-worn,
.loadout-item-text {
  display: flex;
  flex-direction: column;
  min-width: 0;
}

.loadout-item-name {
  font-weight: 600;
}

.loadout-item-sub {
  color: var(--ui-ink-muted);
  font-size: var(--ui-text-caption);
}

/* A bar: its label, the track, its words. The track is for the eye —
   the words carry the numbers — and over is said in words as well as
   drawn in the warning colour. With no capacity set the track is empty
   and dashed. */
.loadout-meter {
  display: grid;
  grid-template-columns: 4rem minmax(3rem, 1fr) auto;
  gap: var(--ui-space-2);
  align-items: center;
  margin-top: var(--ui-space-2);
  font-size: var(--ui-text-small);
}

.loadout-meter-label {
  color: var(--ui-ink-muted);
}

.loadout-meter-text {
  font-family: var(--ui-font-data);
  font-variant-numeric: tabular-nums;
  text-align: end;
  white-space: nowrap;
}

/* The track is the edge's colour, so it reads on a card's sunk surface
   and a panel's raised one alike; the fill is the accent lifted toward
   the ink, as the canvas's lighter teal is, so it stands off the track
   in either theme. */
.loadout-track {
  display: block;
  height: 0.375rem;
  overflow: hidden;
  border-radius: 999px;
  background: var(--ui-edge);
}

.loadout-fill {
  display: block;
  height: 100%;
  border-radius: inherit;
  background: color-mix(in srgb, var(--ui-accent) 55%, var(--ui-ink));
}

.loadout-meter.over .loadout-fill,
.loadout-carry.over .loadout-fill {
  background: var(--ui-warning);
}

.loadout-meter.over .loadout-meter-text,
.loadout-carry.over .loadout-carry-word {
  color: var(--ui-warning);
}

.loadout-meter.unset .loadout-track,
.loadout-carry.unset .loadout-track {
  background: transparent;
  box-shadow: inset 0 0 0 1px var(--ui-edge);
}

.loadout-meter.unset .loadout-meter-text,
.loadout-carry.unset .loadout-carry-word {
  color: var(--ui-ink-muted);
}

/* The carry card: the weight large in the data face, the capacity after
   it, the track, and the word under it. */
.loadout-carry-line {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--ui-space-2);
  margin: 0 0 var(--ui-space-2);
}

.loadout-carry-kg {
  font-family: var(--ui-font-data);
  font-size: var(--ui-text-title);
  font-variant-numeric: tabular-nums;
}

.loadout-carry-of {
  color: var(--ui-ink-muted);
}

.loadout-carry .loadout-track {
  height: 0.5rem;
}

.loadout-carry-word {
  margin: var(--ui-space-2) 0 0;
  font-size: var(--ui-text-small);
}

/* On you, then each place; each container a card, indented by how deep
   it sits in its group, a nested one edged in the accent. */
.loadout-group {
  display: flex;
  flex-direction: column;
  gap: var(--ui-space-2);
}

.loadout-group + .loadout-group {
  margin-top: var(--ui-space-4);
  padding-top: var(--ui-space-3);
  border-top: 1px solid var(--ui-edge);
}

.loadout-card {
  min-width: 0;
  margin-inline-start: calc(var(--loadout-depth, 0) * var(--ui-space-4));
  padding: var(--ui-space-2) var(--ui-space-3);
  background: var(--ui-surface);
  border: 1px solid var(--ui-edge);
  border-radius: var(--ui-radius-md);
}

.loadout-card.nested {
  border-inline-start: 2px solid var(--ui-accent);
}

.loadout-card-head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--ui-space-1) var(--ui-space-2);
}

.loadout-card-name {
  margin: 0;
  font-size: var(--ui-text-body);
  font-weight: 600;
}

.loadout-card-sub,
.loadout-card-holds {
  color: var(--ui-ink-muted);
  font-size: var(--ui-text-caption);
}

.loadout-card-note {
  margin: var(--ui-space-1) 0 0;
  font-size: var(--ui-text-small);
  font-style: italic;
}

.loadout-card-holds {
  margin: var(--ui-space-1) 0 0;
}

/* What a container holds: name and note, class, quantity and weight in
   columns. A rider is one row: a passenger the seat does not play has
   no number (ruling P), so it takes the name column alone; one it plays
   is counted in the bars (U3 (b)), and takes the item's columns — its
   room where the class sits, its weight under theirs. */
.loadout-items {
  margin-top: var(--ui-space-2);
  border-top: 1px solid var(--ui-edge);
}

.loadout-item {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto 2.5rem 4rem;
  gap: var(--ui-space-2);
  align-items: baseline;
  padding: var(--ui-space-1) 0;
  border-bottom: 1px solid var(--ui-edge);
  font-size: var(--ui-text-small);
}

.loadout-item:last-child {
  border-bottom: 0;
}

.loadout-item-class {
  color: var(--ui-ink-muted);
  font-size: var(--ui-text-caption);
}

.loadout-item-qty,
.loadout-item-kg {
  font-family: var(--ui-font-data);
  font-variant-numeric: tabular-nums;
  text-align: end;
  white-space: nowrap;
}

.loadout-rider:not(.counted) {
  grid-template-columns: minmax(0, 1fr);
}

.loadout-empty {
  margin: var(--ui-space-2) 0 0;
  color: var(--ui-ink-muted);
  font-size: var(--ui-text-small);
}

/* The narrow layout (PC4b-4, read at 375 × 812), which the 24rem
   drawer takes at every width (map-view drawers MV1): one column —
   Equipped, then carry, then the containers — less padding round the
   panels and the indent, and item columns that still leave the name
   room. In the left drawer only, as the compact sheet is. */
.drawer[data-slot="left"] :is(.seat-loadout, .seat-loadout-side) {
  grid-template-columns: minmax(0, 1fr);
  gap: var(--ui-space-3);
}

.drawer[data-slot="left"] .loadout-panel {
  padding: var(--ui-space-3) var(--ui-space-2);
}

.drawer[data-slot="left"] .loadout-card {
  margin-inline-start: calc(var(--loadout-depth, 0) * var(--ui-space-2));
  padding: var(--ui-space-2);
}

.drawer[data-slot="left"] .loadout-meter {
  grid-template-columns: 3.5rem minmax(2rem, 1fr) auto;
}

.drawer[data-slot="left"] .loadout-item {
  grid-template-columns: minmax(0, 1fr) auto 2rem 3.25rem;
  gap: var(--ui-space-1);
}

.drawer[data-slot="left"] .loadout-rider:not(.counted) {
  grid-template-columns: minmax(0, 1fr);
}

/* [/PC4] ---------------------------------------------------------- */

/* [C1-inbox — the GM's inbox beside Seats and its top-bar badge
   (player-view C1a-3, inbox_ui.rs)] ---------------------------------
   Reserved by C1's contract (C1-0) ahead of its code. C1a-3 appends its
   rules between this marker and the one that closes it, and edits no
   line outside them.
   --------------------------------------------------------------- */

/* The section takes the Seats section's shape — the same rule above,
   the same head — so the World panel stays one column of sections. */
.inbox-section {
  margin-top: var(--ui-space-3);
  border-top: 1px solid var(--ui-edge);
  padding-top: var(--ui-space-2);
}

.inbox-section .section-head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--ui-space-2);
}

.inbox-section h2 {
  margin: 0;
}

.inbox-section h2 .count {
  margin-left: var(--ui-space-2);
  font-size: var(--ui-text-small);
  color: var(--ui-ink-muted);
}

.inbox-show-answered[aria-pressed="true"] {
  border-color: var(--ui-accent);
}

.inbox-section .status {
  margin: var(--ui-space-1) 0 0;
}

.inbox-section .status.silent {
  display: none;
}

.inbox-list,
.inbox-offers {
  list-style: none;
  margin: var(--ui-space-2) 0 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--ui-space-2);
}

.inbox-row,
.inbox-offer {
  display: flex;
  flex-direction: column;
  gap: var(--ui-space-1);
  border: 1px solid var(--ui-edge);
  border-radius: var(--ui-radius-lg);
  padding: var(--ui-space-2) var(--ui-space-3);
}

/* Answered, while "Show answered" lists it: still readable, stepped
   back — by ink and edge, not by a colour of its own. */
.inbox-row.answered {
  color: var(--ui-ink-muted);
  border-style: dashed;
}

.inbox-row .meta,
.inbox-offer .meta {
  margin: 0;
  font-size: var(--ui-text-small);
  color: var(--ui-ink-muted);
}

.inbox-row-head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--ui-space-2);
}

.inbox-kind {
  font-size: var(--ui-text-small);
  color: var(--ui-ink-muted);
}

/* Who sent it: the one fact every row leads with. */
.inbox-from {
  color: var(--ui-caption);
  font-weight: 600;
}

.inbox-row-head .meta {
  margin-left: auto;
  font-size: var(--ui-text-caption);
  font-variant-numeric: tabular-nums;
}

.inbox-title {
  margin: 0;
  font-weight: 600;
  overflow-wrap: anywhere;
}

.inbox-facts {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: 0 var(--ui-space-2);
  margin: 0;
  font-size: var(--ui-text-small);
}

.inbox-facts > div {
  display: contents;
}

.inbox-facts dt {
  color: var(--ui-ink-muted);
}

.inbox-facts dd {
  margin: 0;
  overflow-wrap: anywhere;
}

/* What a player wrote, as they wrote it: their line breaks kept, and a
   long unbroken word wrapped rather than widening the panel. */
.inbox-body {
  margin: 0;
  font-family: var(--ui-font-prose);
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}

.inbox-raw summary {
  cursor: pointer;
  font-size: var(--ui-text-small);
  color: var(--ui-ink-muted);
}

.inbox-raw pre {
  margin: var(--ui-space-1) 0 0;
  padding: var(--ui-space-2);
  max-height: calc(var(--ui-space-12) * 4);
  overflow: auto;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  font-size: var(--ui-text-caption);
  background: var(--ui-surface);
  border-radius: var(--ui-radius-md);
}

.inbox-actions {
  display: flex;
  gap: var(--ui-space-2);
}

.inbox-offer > button {
  align-self: flex-start;
}

.inbox-subhead {
  margin: var(--ui-space-3) 0 0;
  font-size: var(--ui-text-body);
}

/* The top bar's badge wears Due's pill, so the bar's two counts read
   alike: an edge-grey zero, an accent number when something waits. */
.inbox-badge .count {
  display: inline-block;
  min-width: 1.5em;
  margin-left: var(--ui-space-1);
  padding: 0 var(--ui-space-1);
  border-radius: 1em;
  background: var(--ui-edge);
  text-align: center;
  font-family: var(--ui-font-data);
  font-variant-numeric: tabular-nums;
}

.inbox-badge.has-open .count {
  background: var(--ui-accent);
}

/* [/C1-inbox] ----------------------------------------------------- */

/* [C1-seat — a seat's chat and notes fans (player-view C1a-4,
   seat_chat.rs, seat_notes.rs)] -------------------------------------
   Reserved by C1's contract (C1-0) ahead of its code. C1a-4 appends its
   rules between this marker and the one that closes it, and edits no
   line outside them.
   --------------------------------------------------------------- */

/* The two fans' frame (fan.rs's split: this element is where the fan is
   and which layer it is on, and never moves; the .fan-panel inside
   paints and slides). A wall on the right, as .map-dock is, over the
   panel of what the seat was shown. On the chrome's layer, above the
   map's overlays, so a player can talk while reading a sheet, and under the Tools menu (a menu's), so opening Tools over an
   open chat still shows it. The two share this class and pass their own
   `scope`, so both may be open at once; the notes fan, mounted after the
   chat, lies on top, and Escape closes it first. */
.seat-talk {
  position: fixed;
  top: var(--ui-chrome-top);
  right: 0;
  bottom: 0;
  width: var(--ui-dock-width);
  max-width: 100vw;
  z-index: var(--ui-z-chrome);
  display: flex;
}

.seat-talk .fan-panel {
  border-left: 1px solid var(--ui-edge);
}

/* The edge it comes in from: a fan mounts on open, so the transition
   declared with .fan-panel needs a from-state of its own. */
@starting-style {
  .seat-talk .fan-panel {
    transform: translateX(100%);
  }
}

/* A phone is one column: the fan takes the width under the bar. */
@media (max-width: 640px) {
  .seat-talk {
    left: 0;
    width: auto;
  }

  .seat-talk .fan-panel {
    border-left: none;
    padding: var(--ui-space-2);
  }
}

/* The chat fills the panel: the log takes what the composer leaves and
   scrolls by itself, so the composer stays at the foot. On a screen too
   short for both, the log keeps a few lines and the panel scrolls. */
.talk-chat {
  flex: 1 1 0;
  min-height: 0;
  display: flex;
  flex-direction: column;
  gap: var(--ui-space-2);
}

/* `column-reverse` anchors the scroll at the foot: a new post shows
   without any script moving the scroll, and a reader scrolled back up
   stays where they are. The list inside keeps its own order, so the
   document (and a screen reader) reads oldest first. */
.talk-log {
  flex: 1 1 0;
  min-height: 6rem;
  overflow-y: auto;
  display: flex;
  flex-direction: column-reverse;
  border: 1px solid var(--ui-edge);
  border-radius: var(--ui-radius-md);
  background: var(--ui-surface-raised);
}

.talk-lines {
  list-style: none;
  margin: 0;
  padding: var(--ui-space-2);
  display: flex;
  flex-direction: column;
  gap: var(--ui-space-2);
}

.talk-line {
  padding-left: var(--ui-space-2);
  border-left: 2px solid transparent;
}

.talk-line.mine {
  border-left-color: var(--ui-accent);
}

.talk-head,
.note-head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0 var(--ui-space-2);
  margin: 0;
}

.talk-head .who,
.note-title {
  font-weight: 600;
  overflow-wrap: anywhere;
}

.talk-head .also,
.note-whose {
  color: var(--ui-ink-muted);
  font-size: var(--ui-text-small);
  overflow-wrap: anywhere;
}

.talk-head .at,
.note-head .at {
  margin-left: auto;
  color: var(--ui-ink-muted);
  font-family: var(--ui-font-data);
  font-size: var(--ui-text-caption);
  font-variant-numeric: tabular-nums;
}

.talk-tag {
  padding: 0 var(--ui-space-1);
  border: 1px solid var(--ui-edge);
  border-radius: var(--ui-radius-sm);
  color: var(--ui-ink-muted);
  font-family: var(--ui-font-data);
  font-size: var(--ui-text-caption);
  text-transform: uppercase;
  letter-spacing: 0.06em;
}

/* What was said, as it was typed: its line breaks kept, and a long word
   broken rather than widening the fan. */
.talk-body {
  margin: 0;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}

.talk-line.emote .talk-body {
  font-style: italic;
}

.talk-line.ooc .talk-body {
  color: var(--ui-ink-muted);
}

.talk-empty {
  margin: 0;
  padding: var(--ui-space-2);
  color: var(--ui-ink-muted);
}

.talk-empty.silent,
.talk-stale.silent {
  display: none;
}

.talk-stale {
  margin: 0;
}

/* The composer: how it is said, who says it, what is said. */
.talk-compose {
  display: flex;
  flex-direction: column;
  gap: var(--ui-space-2);
}

.talk-compose .field-col {
  margin: 0;
}

.talk-compose input[type="text"],
.talk-compose select,
.talk-compose textarea {
  width: 100%;
}

.talk-compose textarea {
  resize: vertical;
}

.talk-compose .status {
  margin: 0;
}

.talk-kinds {
  display: flex;
  gap: var(--ui-space-1);
}

.talk-kinds button[aria-pressed="true"] {
  background: var(--ui-accent);
  border-color: var(--ui-accent);
}

/* The name and the character side by side while they fit, stacked on a
   phone. */
.talk-who {
  display: flex;
  flex-wrap: wrap;
  gap: var(--ui-space-2);
}

.talk-who .field-col {
  flex: 1 1 8rem;
  min-width: 0;
}

.talk-send {
  display: flex;
  align-items: center;
  gap: var(--ui-space-2);
}

.talk-count {
  margin-right: auto;
  color: var(--ui-ink-muted);
  font-family: var(--ui-font-data);
  font-size: var(--ui-text-caption);
  font-variant-numeric: tabular-nums;
}

/* The notes fan: the composer first, then what the seat can read. The
   panel scrolls as one. */
.note-compose h4,
.note-read h4 {
  margin: 0;
}

.note-scope {
  margin: 0;
  padding: var(--ui-space-2);
  border: 1px solid var(--ui-edge);
  border-radius: var(--ui-radius-md);
  display: flex;
  flex-direction: column;
  gap: var(--ui-space-1);
}

.note-scope legend {
  padding: 0 var(--ui-space-1);
  color: var(--ui-ink-muted);
  text-transform: uppercase;
  font-size: var(--ui-text-small);
  letter-spacing: 0.12em;
}

.note-scope label {
  display: flex;
  align-items: baseline;
  gap: var(--ui-space-2);
}

.note-read {
  display: flex;
  flex-direction: column;
  gap: var(--ui-space-2);
  margin-top: var(--ui-space-2);
  padding-top: var(--ui-space-2);
  border-top: 1px solid var(--ui-edge);
}

.note-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--ui-space-2);
}

.note-item {
  padding: var(--ui-space-2);
  border: 1px solid var(--ui-edge);
  border-left-width: 2px;
  border-radius: var(--ui-radius-md);
  background: var(--ui-surface-raised);
}

.note-item.mine {
  border-left-color: var(--ui-accent);
}

.note-whose {
  margin: 0 0 var(--ui-space-1);
}

/* The fold (2026-09-27): Chat, Notes and Shopping behind one Messages
   button, the bar's last control (player_shell.rs's `Talk`).

   The button sits at the far end of the bar whether or not the map's
   tools are there: while they are, .bar-map's auto margin pushes both
   over; while a tab covers the map they are gone, and this takes the
   margin itself. It is painted as the bar's pressed doors are while
   its menu is open or one of its fans is — the doors it replaced were
   pressed while theirs was. */
.player-bar .talk-door {
  margin-left: auto;
}

.player-bar .bar-map + .talk-door {
  margin-left: 0;
}

.player-bar .talk-door[aria-expanded="true"],
.player-bar .talk-door.fan-open {
  background: var(--ui-accent);
  border-color: var(--ui-accent);
}

/* The bar on a phone, which the fold is for: it fits 375px without
   scrolling, by two gives in a strict order. The wordmark and the seat
   line share one box, the only thing in the bar that shrinks, and it
   is a wrapping row that clips its second line — .topbar h1's own
   wrap, a line up. So the seat line's ellipsis runs down to four ems;
   short of that it drops to the clipped line and goes whole; and only
   then, short of the wordmark's width, "Play" goes whole too, rather
   than the bar scrolling Messages off its edge. The play surface's
   block keeps the wordmark from shrinking, and the seat line keeps its
   ellipsis from there. */
.player-bar .player-names {
  flex: 0 1 auto;
  min-width: 0;
  align-self: stretch;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  overflow: hidden;
}

/* As tall as the bar and no wider: while the first line holds this,
   anything that does not fit beside it wraps, below the clip. */
.player-bar .player-names::before {
  content: "";
  height: 100%;
}

.player-bar .player-seat {
  flex: 1 1 4em;
  max-width: max-content;
  margin-left: var(--ui-space-3);
}

/* And the bar's own two buttons take the tabs' narrow padding, with
   the gaps a step closer — without which the three tabs, Tools and
   Messages alone come to 384px at 375. */
@media (max-width: 640px) {
  .player-bar {
    column-gap: var(--ui-space-2);
  }

  .player-bar .bar-map button,
  .player-bar .talk-door {
    padding: var(--ui-space-1) var(--ui-space-2);
  }
}

/* The menu takes the shape of the surface's other menu, the Tools fan
   (.player-fan, which hangs from the same corner and takes turns with
   it): fixed under the bar at the right, on the menus' layer, a card
   with an edge, a corner and a shadow. Outside the bar in the
   document, so this layer ranks against the fans' — the bar is a
   stacking context of its own at the chrome's. It wears .fan for the
   focus rule only; nothing in the stylesheet reads that class. */
.talk-menu {
  position: fixed;
  top: var(--ui-chrome-top);
  right: var(--ui-space-3);
  z-index: var(--ui-z-menu);
  min-width: 10rem;
  max-width: calc(100vw - 2 * var(--ui-space-3));
  margin: 0;
  padding: var(--ui-space-2);
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: var(--ui-space-1);
  background: var(--ui-surface);
  border: 1px solid var(--ui-edge);
  border-radius: var(--ui-radius-lg);
  box-shadow: var(--ui-elevation-2);
}

/* Each item a full-width row a thumb can hit. */
.talk-menu button {
  width: 100%;
  min-height: 2.75rem;
  text-align: left;
}

.talk-menu button[aria-pressed="true"] {
  background: var(--ui-accent);
  border-color: var(--ui-accent);
}

/* [/C1-seat] ------------------------------------------------------ */

/* [C1-offer — a seat's offer card and the GM's "Offer to…"
   (player-view C1b-1, seat_offer.rs, rules_ui.rs)] ------------------
   Reserved by C1's contract (C1-0) ahead of its code. C1b-1 appends its
   rules between this marker and the one that closes it, and edits no
   line outside them.
   --------------------------------------------------------------- */

/* The GM's side: "Offer to…" inside the compose step (rules_ui.rs). It
   sits in the roll bar, so the bar's own label, .check and .field-row
   rules already dress its controls; this is only its frame. */
.offer-to {
  display: flex;
  flex-direction: column;
  gap: var(--ui-space-2);
  padding-top: var(--ui-space-2);
  border-top: 1px dashed var(--ui-edge);
}

.offer-choice {
  margin: 0;
  padding: var(--ui-space-2);
  border: 1px solid var(--ui-edge);
  border-radius: var(--ui-radius-md);
  display: flex;
  flex-direction: column;
  gap: var(--ui-space-1);
}

.offer-choice legend {
  padding: 0 var(--ui-space-1);
  color: var(--ui-ink-muted);
  text-transform: uppercase;
  font-size: var(--ui-text-small);
  letter-spacing: 0.12em;
}

/* The seats, indented under the choice that shows them. */
.offer-seats {
  display: flex;
  flex-direction: column;
  gap: var(--ui-space-1);
  padding-left: var(--ui-space-3);
  border-left: 2px solid var(--ui-edge);
}

.offer-none,
.offer-said {
  margin: 0;
  color: var(--ui-ink-muted);
  font-size: var(--ui-text-caption);
}

/* The seat's side: the open offers, as cards over the map
   (seat_offer.rs). Bottom left, clear of the panel of what the seat was
   shown on the right (--ui-chrome-right is its width), and above the
   Characters tray (--drawer-bottom-reach, map-view drawers MV1), in the
   drawers' layer over every panel and under every tab ([MV1]), so an
   offer that arrives while a sheet is open is seen over it. The stack scrolls as
   one when several are open. Never over a drawer's tabs or an open
   one's head and Close, though: its height is capped at the room under
   them, --drawer-room, which drawers.rs measures on the stack, and the
   rules below set only the cap of their own, --offer-max. A 3rem floor
   keeps the card's head in view where a deep drawer leaves less. */
.offer-stack {
  position: fixed;
  left: var(--ui-space-3);
  bottom: calc(var(--ui-space-3) + var(--drawer-bottom-reach, 0px));
  z-index: var(--ui-z-chrome);
  width: min(26rem, calc(100vw - var(--ui-chrome-right) - 2 * var(--ui-space-3)));
  --offer-max: calc(100vh - var(--ui-chrome-top) - var(--drawer-bottom-reach, 0px) - 4 * var(--ui-space-3));
  max-height: min(var(--offer-max), max(3rem, var(--drawer-room, 100vh)));
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  gap: var(--ui-space-2);
}

.offer-card,
.offer-receipt {
  display: flex;
  flex-direction: column;
  gap: var(--ui-space-2);
  padding: var(--ui-space-3);
  border: 1px solid var(--ui-edge);
  border-left: 3px solid var(--ui-accent);
  border-radius: var(--ui-radius-lg);
  background: var(--ui-surface);
  box-shadow: var(--ui-elevation-2);
}

.offer-receipt {
  border-left-color: var(--ui-edge);
  gap: var(--ui-space-1);
}

.offer-head,
.offer-receipt-head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0 var(--ui-space-2);
  margin: 0;
}

.offer-head h3 {
  margin: 0;
  font-size: var(--ui-text-lead);
  overflow-wrap: anywhere;
}

/* Where focus goes when a card's button leaves with its send, and when
   a card leaves the stack with focus in it (the C1b review's walk). */
.offer-head h3:focus-visible,
.offer-receipt:focus-visible {
  outline: 2px solid var(--ui-accent);
  outline-offset: 2px;
}

.offer-tag {
  color: var(--ui-ink-muted);
  text-transform: uppercase;
  font-size: var(--ui-text-caption);
  letter-spacing: 0.12em;
}

.offer-head .offer-tag {
  flex-basis: 100%;
}

.offer-rule {
  color: var(--ui-ink-muted);
  font-family: var(--ui-font-data);
  font-size: var(--ui-text-caption);
}

.offer-receipt-title {
  font-weight: 600;
  overflow-wrap: anywhere;
}

.offer-dismiss {
  margin-left: auto;
  padding: 0 var(--ui-space-2);
  font-size: var(--ui-text-caption);
}

/* The GM's line reads as prose, as it does on the roll bar. */
.offer-note {
  margin: 0;
  font-family: var(--ui-font-prose);
  font-style: italic;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}

.offer-terms,
.offer-flat,
.offer-hint,
.offer-pool {
  margin: 0;
  color: var(--ui-ink-muted);
  font-size: var(--ui-text-caption);
}

.offer-pool {
  font-family: var(--ui-font-data);
}

.offer-fill {
  margin: 0;
  padding: 0;
  border: 0;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: var(--ui-space-2);
}

.offer-card .field-row,
.offer-card .check {
  display: inline-flex;
  align-items: baseline;
  gap: var(--ui-space-2);
  margin: 0;
}

.offer-card .field-row > span {
  color: var(--ui-ink-muted);
  text-transform: uppercase;
  font-size: var(--ui-text-small);
  letter-spacing: 0.12em;
}

.offer-card .num {
  width: 4rem;
}

.offer-slots {
  display: flex;
  flex-direction: column;
  gap: var(--ui-space-2);
}

.offer-slot,
.offer-row,
.offer-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--ui-space-2);
}

.offer-slot {
  padding-bottom: var(--ui-space-2);
  border-bottom: 1px solid var(--ui-edge);
}

.offer-slot .candidates {
  display: flex;
  flex-wrap: wrap;
  gap: var(--ui-space-1);
}

/* The trait in the pool, in the selection colour the roll bar uses. */
.offer-slot .candidates button[aria-pressed="true"] {
  border-color: var(--ui-select);
  color: var(--ui-select);
}

/* "More options…" is quieter than the GM's candidates, as on the roll
   bar: the door to everything, not one more suggestion. */
.offer-slot .candidates .more {
  border-style: dashed;
  border-color: var(--ui-edge);
  color: var(--ui-ink-muted);
}

.offer-other {
  flex-basis: 100%;
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--ui-space-2);
  padding-left: var(--ui-space-3);
  border-left: 2px solid var(--ui-edge);
}

.offer-other input {
  width: 10rem;
}

.offer-actions button.primary {
  border-color: var(--ui-accent);
}

.offer-actions button:disabled,
.offer-fill:disabled {
  opacity: 0.6;
}

/* The dice drawn, loud as the roll bar's are. */
.offer-result,
.offer-receipt .dice,
.offer-receipt .counts {
  margin: 0;
}

.offer-result {
  display: flex;
  flex-direction: column;
  gap: var(--ui-space-1);
  padding-top: var(--ui-space-2);
  border-top: 1px solid var(--ui-edge);
}

.offer-result .dice,
.offer-receipt .dice {
  margin: 0;
  color: var(--ui-select);
  font-family: var(--ui-font-data);
  font-size: var(--ui-text-lead);
  letter-spacing: 0.08em;
}

.offer-result .counts,
.offer-receipt .counts {
  display: flex;
  flex-wrap: wrap;
  gap: var(--ui-space-3);
  margin: 0;
}

.offer-result .outcome,
.offer-receipt .outcome {
  font-variant: small-caps;
  letter-spacing: 0.06em;
}

.offer-status,
.offer-stale {
  margin: 0;
}

.offer-stale.silent {
  display: none;
}

/* A card's status line is a live region, so while it has nothing to say
   it stays in the accessibility tree — one that appears from display:
   none with its words is announced by nothing — and out of the card's
   flex flow, so it adds no gap. `.status`'s min-height would outgrow
   the clip. */
.offer-status.silent {
  position: absolute;
  width: 1px;
  height: 1px;
  min-height: 0;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* A phone: the stack takes the width under the bar, and leaves the top
   half of the map in view. */
@media (max-width: 640px) {
  .offer-stack {
    left: var(--ui-space-2);
    right: var(--ui-space-2);
    bottom: calc(var(--ui-space-2) + var(--drawer-bottom-reach, 0px));
    width: auto;
    --offer-max: min(55vh, calc(100vh - var(--ui-chrome-top) - var(--drawer-bottom-reach, 0px) - 4 * var(--ui-space-2)));
  }

  .offer-card,
  .offer-receipt {
    padding: var(--ui-space-2);
  }

  /* A phone has one wall, and an open Messages fan (.seat-talk, which
     is mounted only while open) takes all of it: the stack, on the same
     layer and painted later, would lie over the fan's composer and its
     Send. So while one is open the stack steps out — display: none, so
     its controls leave the tab order too — and comes back as the fan
     shuts, the card as it was. The count a screen reader hears lives
     outside the stack (seat_offer.rs) and is still said. */
  .player-shell:has(.seat-talk) .offer-stack {
    display: none;
  }
}

/* [/C1-offer] ----------------------------------------------------- */

/* [C1-shop — a seat's shopping requests and the store panel's list of
   them (player-view C1b-2, seat_requests.rs, stores_ui.rs)] ---------
   Reserved by C1's contract (C1-0) ahead of its code. C1b-2 appends its
   rules between this marker and the one that closes it, and edits no
   line outside them.
   --------------------------------------------------------------- */

/* The shopping fan wears the chat and notes fans' frame and composer
   (.seat-talk, .talk-compose, .note-read and .note-list, [C1-seat]) —
   it takes turns with them on one wall. What is its own is below. */
.shop-compose h4,
.shop-sent h4,
.shop-lede {
  margin: 0;
}

/* The two bands and the count side by side while they fit, stacked on a
   phone; the count is narrow, being three digits at most. */
.shop-bands {
  display: flex;
  flex-wrap: wrap;
  gap: var(--ui-space-2);
}

.shop-bands .field-col {
  flex: 1 1 7rem;
  min-width: 0;
  margin: 0;
}

.shop-bands .field-col > span {
  white-space: nowrap;
}

.shop-bands .shop-qty {
  flex: 0 0 6.5rem;
}

/* The store panel's requests, above Buy: a card each, marked on the
   left as a seat's own lines are on their side. The heading is Buy's
   (.buy-head). */
.place-store .store-requests > .meta {
  margin: 0 0 0.4rem;
  font-size: var(--ui-text-small);
}

.place-store .store-requests .buy-head .count {
  margin-left: 0.4rem;
  opacity: 0.7;
}

.place-store .request-list {
  gap: var(--ui-space-2);
}

.request-row {
  display: flex;
  flex-direction: column;
  gap: var(--ui-space-1);
  padding: var(--ui-space-2);
  border: 1px solid var(--ui-edge);
  border-left: 2px solid var(--ui-accent);
  border-radius: var(--ui-radius-md);
  background: var(--ui-surface-raised);
}

.request-row p {
  margin: 0;
}

.request-row .meta {
  color: var(--ui-ink-muted);
  font-size: var(--ui-text-small);
  overflow-wrap: anywhere;
}

.request-head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0 0.6rem;
}

.request-head .name {
  font-weight: 600;
  overflow-wrap: anywhere;
}

/* The player's note as they typed it: its line breaks kept, a long word
   broken rather than widening the panel. */
.request-note {
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}

.request-actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--ui-space-1);
}

/* What Mark resolved settled to, under the list and above Buy: taking
   no room until there is something to say, and never display: none — a
   live region has to be in the tree before its words are, to be read. */
.place-store .request-status {
  margin: 0;
}

.place-store .request-status.silent {
  min-height: 0;
  padding: 0;
}

/* [/C1-shop] ------------------------------------------------------ */

/* [PC5 — what is worn, on the figure (player-character-sheet PC5,
   seat_figure.rs)] --------------------------------------------------
   Reserved by wave 4's setup ahead of its code. PC5 appends its rules
   between this marker and the one that closes it, and edits no line
   outside them. The Loadout carries `.figure` while the figure shows.
   --------------------------------------------------------------- */

/* The List · Figure pair, in the shape of the sheet's View/Edit pair. */
.figure-switch {
  display: inline-flex;
}

.figure-switch button {
  border-radius: 0;
}

.figure-switch button:first-child {
  border-radius: var(--ui-radius-md) 0 0 var(--ui-radius-md);
}

.figure-switch button:last-child {
  border-left: 0;
  border-radius: 0 var(--ui-radius-md) var(--ui-radius-md) 0;
}

.figure-switch button[aria-pressed="true"] {
  background: var(--ui-accent);
  border-color: var(--ui-accent);
}

/* With the figure showing, the Loadout is one column — the side column,
   Equipped in it, above the containers — so the figure has the width for
   its callouts. Keyed on the panel being there: a car has no Equipped
   panel, keeps `.figure` from the pick, and keeps its two columns. */
.seat-loadout.figure:has(.loadout-equipped) {
  grid-template-columns: minmax(0, 1fr);
}

/* The figure. Every number it is laid out with is seat_figure.rs's,
   handed over as a custom property in sixteenths of a rem; --figure-u is
   that unit, so the canvas scales with the UI. Narrow by default: the
   outline beside its callouts, stacked head to foot, and no leaders. */
.seat-figure {
  --figure-u: 0.0625rem;
  container-type: inline-size;
  display: flex;
  flex-direction: column;
  gap: var(--ui-space-3);
  min-width: 0;
}

.figure-canvas {
  display: grid;
  grid-template-columns: 5rem minmax(0, 1fr);
  gap: var(--ui-space-3);
  align-items: start;
}

.figure-body {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: var(--figure-body-w) / var(--figure-body-h);
}

.figure-outline {
  fill: none;
  stroke: var(--ui-ink-muted);
  stroke-opacity: 0.6;
  stroke-width: 1.5;
  stroke-linecap: round;
  stroke-linejoin: round;
  vector-effect: non-scaling-stroke;
}

/* An anchor: a faint dot where nothing is worn, a ring in the list's
   slot gold where something is. Back's ring is dashed — it is behind. */
.figure-anchor {
  fill: var(--ui-ink-muted);
  opacity: 0.7;
}

.figure-anchor.worn {
  fill: var(--ui-surface);
  stroke: var(--ui-select);
  stroke-width: 2;
  opacity: 1;
  vector-effect: non-scaling-stroke;
}

.figure-anchor.worn.behind {
  stroke-dasharray: 3 2;
}

.figure-leaders {
  display: none;
}

.figure-leaders path {
  fill: none;
  stroke: var(--ui-edge);
  stroke-width: 1;
  vector-effect: non-scaling-stroke;
}

.figure-callouts,
.figure-things {
  margin: 0;
  padding: 0;
  list-style: none;
}

.figure-callouts {
  display: flex;
  flex-direction: column;
  gap: var(--ui-space-2);
  min-width: 0;
}

/* A callout: the region's name in the panels' small caps, the slot as
   written in the list's gold, then what is worn there. */
.figure-callout {
  display: flex;
  flex-direction: column;
  min-width: 0;
  padding: var(--ui-space-1) var(--ui-space-2);
  background: var(--ui-surface);
  border: 1px solid var(--ui-edge);
  border-radius: var(--ui-radius-lg);
}

.figure-region {
  color: var(--ui-ink-muted);
  font-size: var(--ui-text-caption);
  font-weight: 600;
  letter-spacing: 0.1em;
  text-transform: uppercase;
}

.figure-slot {
  color: var(--ui-select);
  font-size: var(--ui-text-caption);
  font-weight: 600;
}

.figure-thing {
  font-size: var(--ui-text-small);
}

.figure-thing-name {
  font-weight: 600;
}

.figure-thing-sub {
  color: var(--ui-ink-muted);
  font-size: var(--ui-text-caption);
}

.figure-other-head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--ui-space-1) var(--ui-space-2);
  margin-bottom: var(--ui-space-2);
}

/* Wide: the design's canvas, once the figure's own box holds it. Each
   callout stands where lay() put it, beside the part it names, with its
   leader drawn to the anchor. Its rows are the heights lay() counted,
   one line each, cut short with an ellipsis rather than wrapped. */
@container (min-width: 50rem) {
  .figure-canvas {
    display: block;
    position: relative;
    width: calc(var(--figure-w) * var(--figure-u));
    height: calc(var(--figure-h) * var(--figure-u));
    margin-inline: auto;
  }

  .figure-body {
    position: absolute;
    left: calc(var(--figure-body-x) * var(--figure-u));
    top: calc(var(--figure-body-y) * var(--figure-u));
    width: calc(var(--figure-body-w) * var(--figure-u));
    height: calc(var(--figure-body-h) * var(--figure-u));
  }

  .figure-leaders {
    display: block;
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
  }

  .figure-callouts {
    display: block;
  }

  .figure-callout {
    display: block;
    position: absolute;
    left: calc(var(--figure-x) * var(--figure-u));
    top: calc(var(--figure-top) * var(--figure-u));
    width: calc(var(--figure-callout-w) * var(--figure-u));
    height: calc(var(--figure-height) * var(--figure-u));
    padding: calc(var(--figure-pad) * var(--figure-u)) calc(10 * var(--figure-u));
    overflow: hidden;
  }

  .figure-region,
  .figure-slot,
  .figure-thing {
    display: block;
    overflow: hidden;
    white-space: nowrap;
    text-overflow: ellipsis;
  }

  .figure-region {
    height: calc(var(--figure-head) * var(--figure-u));
    line-height: calc(var(--figure-head) * var(--figure-u));
  }

  .figure-slot {
    height: calc(var(--figure-slot) * var(--figure-u));
    line-height: calc(var(--figure-slot) * var(--figure-u));
  }

  .figure-thing {
    height: calc(var(--figure-thing) * var(--figure-u));
    line-height: calc(var(--figure-thing) * var(--figure-u));
  }
}

/* The phone (PC5-4, read at 375 × 812): the switch as tall as the
   Loadout's other chips, and the outline a little narrower so the
   callouts keep the room. At 375 the figure's box is about 341px — the
   column's and the panel's phone padding and the panel's border off —
   and the narrow layout's only fixed widths are the outline's column
   and the gap (72 + 8), so nothing reaches the container query's 50rem
   and nothing scrolls sideways. */
@media (max-width: 640px) {
  .figure-switch button {
    min-height: 2.75rem;
  }

  .figure-canvas {
    grid-template-columns: 4.5rem minmax(0, 1fr);
    gap: var(--ui-space-2);
  }
}

/* [/PC5] ---------------------------------------------------------- */

/* [PC6 — the seat's own money (player-character-sheet PC6,
   seat_money.rs)] ---------------------------------------------------
   Reserved by wave 4's setup ahead of its code. PC6 appends its rules
   between this marker and the one that closes it, and edits no line
   outside them.
   --------------------------------------------------------------- */

/* The card and the view borrow the Loadout's panel, head and group
   heading; what is here is money's own: a label and a sum on a line,
   the stacks, the totals and the way in and back. Sums are the data
   face, tabular, and never wrap mid-figure; a where-path wraps
   anywhere rather than widening the page. `.money-line` is scoped to
   the seat's tab: the GM's entity card carries a `.money-line` of its
   own (money_ui.rs's `EntityMoneyLine`). */
.seat-inventory .money-line {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: 0 var(--ui-space-2);
  margin: 0;
  font-size: var(--ui-text-small);
}

.money-line-label {
  color: var(--ui-ink-muted);
}

.money-here {
  font-size: var(--ui-text-body);
}

.money-here .money-line-label {
  color: var(--ui-ink);
  font-weight: 600;
}

.money-amount {
  font-family: var(--ui-font-data);
  font-variant-numeric: tabular-nums;
  text-align: end;
  white-space: nowrap;
}

.money-stacks {
  margin: var(--ui-space-2) 0 0;
  padding: 0;
  list-style: none;
}

.money-stack {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  gap: var(--ui-space-2);
  align-items: baseline;
  padding: var(--ui-space-1) 0;
  border-bottom: 1px solid var(--ui-edge);
  font-size: var(--ui-text-small);
}

.money-stack:last-child {
  border-bottom: 0;
}

.money-stack-path {
  min-width: 0;
  overflow-wrap: anywhere;
}

/* A long sum in another currency takes its own line under the path
   rather than squeezing it. */
.money-stack .money-amount {
  min-width: 0;
  white-space: normal;
}

.money-card .money-line + .money-line,
.money-card .money-stacks + .money-line,
.money-card .money-more + .money-line {
  margin-top: var(--ui-space-2);
}

.money-bank-line {
  padding-top: var(--ui-space-2);
  border-top: 1px solid var(--ui-edge);
}

.money-more {
  margin: var(--ui-space-1) 0 0;
  color: var(--ui-ink-muted);
  font-size: var(--ui-text-caption);
}

/* The Money view: in the Loadout's place, one column. */
.seat-money {
  display: flex;
  flex-direction: column;
  gap: var(--ui-space-4);
  min-width: 0;
}

.money-back {
  align-self: flex-start;
}

.money-totals {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(11rem, 1fr));
  gap: var(--ui-space-2);
  margin: 0;
  padding: 0;
  list-style: none;
}

.money-total {
  display: flex;
  flex-direction: column;
  gap: var(--ui-space-1);
  min-width: 0;
  padding: var(--ui-space-2) var(--ui-space-3);
  background: var(--ui-surface-raised);
  border: 1px solid var(--ui-edge);
  border-radius: var(--ui-radius-md);
  overflow-wrap: anywhere;
}

.money-total-label {
  color: var(--ui-ink-muted);
  font-size: var(--ui-text-caption);
  font-weight: 600;
  letter-spacing: 0.1em;
  text-transform: uppercase;
}

/* Two abreast on a phone: a total may break between its figure and
   its label rather than run out of its card. */
.money-total .money-amount {
  font-size: var(--ui-text-lead);
  text-align: start;
  white-space: normal;
}

.money-total-sub {
  color: var(--ui-ink-muted);
  font-size: var(--ui-text-caption);
}

.money-group + .money-group {
  margin-top: var(--ui-space-3);
}

.money-group-head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: 0 var(--ui-space-2);
  margin: 0;
  font-size: var(--ui-text-small);
  font-weight: 400;
}

.money-foot {
  margin: 0;
  color: var(--ui-ink-muted);
  font-size: var(--ui-text-caption);
}

/* The phone (read at 375 × 812): the totals two abreast, and buttons a
   thumb can hit. */
@media (max-width: 640px) {
  .seat-money {
    gap: var(--ui-space-3);
  }

  .money-totals {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }

  .money-open,
  .money-back {
    min-height: 2.75rem;
  }
}

/* [/PC6] ---------------------------------------------------------- */

/* [W4-shops — store stock on the seat: the store panel's reveal
   control and the seat panel's Shops list] --------------------------
   Reserved by wave 4's setup ahead of its code. The shops stream
   appends its rules between this marker and the one that closes it,
   and edits no line outside them.
   --------------------------------------------------------------- */

/* The line under a store's reveal control saying why the picked seat
   cannot see it: close under the control it qualifies. */
.reveal-control + .reveal-caveat {
  margin-top: var(--ui-space-1);
}

/* The seat's Shops list (PlayerPanel): each shop a card of the panel's
   own list, its lines under it as quiet rows — the name, then family,
   bands and price as one dotted line. A long name or label wraps
   rather than pushing the rail wide, so a phone never scrolls
   sideways. */
.player-list.shops li {
  min-width: 0;
  overflow-wrap: anywhere;
}

.player-list.shops .shop-lines {
  list-style: none;
  margin: var(--ui-space-2) 0 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--ui-space-1);
}

.player-list.shops .shop-lines li {
  border: 0;
  border-top: 1px solid var(--ui-edge);
  border-radius: 0;
  padding: var(--ui-space-1) 0 0;
}

.player-list.shops .line-name {
  display: block;
}

.player-list.shops .line-meta {
  display: block;
  font-size: var(--ui-text-small);
  color: var(--ui-ink-muted);
  font-variant-numeric: tabular-nums;
}

.player-list.shops .shop-empty {
  margin: var(--ui-space-1) 0 0;
  font-size: var(--ui-text-small);
  color: var(--ui-ink-muted);
}

/* What the kind carries without listing it, under the shop's own lines:
   a small heading, then one quiet row a good — its name, and its price
   kept whole at the row's end, as the GM's catch-all rows read. */
.player-list.shops .shop-stock-head {
  margin: var(--ui-space-2) 0 0;
  font-size: var(--ui-text-small);
  font-weight: bold;
  color: var(--ui-ink-muted);
}

.player-list.shops .shop-stock {
  list-style: none;
  margin: var(--ui-space-1) 0 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 0;
  font-size: var(--ui-text-small);
}

.player-list.shops .shop-stock li {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: var(--ui-space-2);
  border: 0;
  border-radius: 0;
  padding: 0;
}

.player-list.shops .stock-name {
  min-width: 0;
}

.player-list.shops .stock-price {
  flex: none;
  white-space: nowrap;
  color: var(--ui-ink-muted);
  font-variant-numeric: tabular-nums;
}

/* [/W4-shops] ----------------------------------------------------- */

/* [A2 — deciding between two copies at boot (campaign-files A2-2)] --
   Reserved by wave 4's setup ahead of its code. A2-2 appends its rules
   between this marker and the one that closes it, and edits no line
   outside them.
   --------------------------------------------------------------- */

/* The dialog stands in for the app, which has not drawn: nothing is
   behind it, so it needs no scrim, and it is laid out in the page's
   own column rather than floated over one. */
.copies-dialog {
  border: 1px solid var(--ui-warning);
  border-radius: var(--ui-radius-lg);
  background: var(--ui-surface-raised);
  box-shadow: var(--ui-elevation-2);
  padding: var(--ui-space-4) var(--ui-space-5);
}

.copies-dialog h2 {
  margin-top: 0;
}

/* The two copies side by side, never ranked: same weight, same colour,
   file first. A row whose numbers differ is marked, not the copy. */
.copies-facts {
  border-collapse: collapse;
  margin: var(--ui-space-3) 0;
  font-family: var(--ui-font-data);
}

.copies-facts th,
.copies-facts td {
  text-align: left;
  vertical-align: top;
  padding: var(--ui-space-1) var(--ui-space-4) var(--ui-space-1) 0;
  border-bottom: 1px solid var(--ui-edge);
}

.copies-facts th[scope="row"] {
  color: var(--ui-ink-muted);
  font-weight: normal;
}

.copies-facts tr.differs td {
  color: var(--ui-warning);
  font-weight: 600;
}

.copies-facts code {
  display: block;
  font-size: var(--ui-text-caption);
  color: var(--ui-ink-muted);
}

.copies-note {
  color: var(--ui-ink-muted);
  font-size: var(--ui-text-small);
}

.copies-actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--ui-space-3);
  margin-top: var(--ui-space-3);
}

/* The default answer carries the accent; a choice that writes is an
   ordinary button until it is confirmed. */
.copies-neither {
  border-color: var(--ui-accent);
  background: var(--ui-accent);
}

.copies-go {
  border-color: var(--ui-warning);
}

.copies-confirm p {
  margin-bottom: 0;
}

.copies-undecided code {
  overflow-wrap: anywhere;
}

/* [/A2] ----------------------------------------------------------- */

/* [PC10 — moves, equip and the item editor on a seat's Inventory tab
   (player-character-sheet PC10)] ------------------------------------
   Only the seat's own page draws these: View as… has no sender, so no
   row drags and no row has controls. A row whose move is out is busy,
   as a played tracker is (PC8): dimmed, with a progress cursor, its
   buttons aria-disabled so they keep their focus. */
.loadout-item > .loadout-row-actions,
.loadout-item > .loadout-move-list {
  grid-column: 1 / -1;
}

.loadout-row-actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--ui-space-1);
}

.loadout-card-head .loadout-row-actions {
  margin-inline-start: auto;
}

.loadout-worn .loadout-row-actions {
  margin-top: var(--ui-space-1);
}

.loadout-row-action {
  font-size: var(--ui-text-caption);
}

.loadout-row-action[aria-disabled="true"] {
  opacity: 0.55;
  cursor: progress;
}

.loadout-item.busy,
.loadout-worn.busy,
.loadout-card-head.busy {
  opacity: 0.6;
  cursor: progress;
}

.loadout-move-list {
  display: flex;
  flex-wrap: wrap;
  gap: var(--ui-space-1);
  margin: var(--ui-space-1) 0 0;
  padding: var(--ui-space-2);
  list-style: none;
  background: var(--ui-surface-raised);
  border: 1px solid var(--ui-edge);
  border-radius: var(--ui-radius-md);
}

.seat-loadout [draggable="true"] {
  cursor: grab;
}

/* While a drag is on, every place that may take a drop is outlined. */
.seat-loadout.dragging .loadout-card,
.seat-loadout.dragging .loadout-slot,
.seat-loadout.dragging .figure-callout {
  outline: 1px dashed var(--ui-select);
  outline-offset: 2px;
}

/* A region's drop target: wider than its anchor, painted only while a
   drag is on. */
.figure-drop {
  fill: transparent;
  pointer-events: all;
}

.seat-loadout.dragging .figure-drop {
  fill: var(--ui-select);
  fill-opacity: 0.15;
}

/* A worn thing's controls on the figure (the review's F5): a row of
   their own under it; on the wide figure exactly the height its callout
   was laid out with, and its "Move to…" list opens under the figure. */
.figure-thing-actions .loadout-row-actions {
  flex-wrap: nowrap;
}

.seat-figure > .loadout-move-list {
  margin-top: var(--ui-space-2);
}

@container (min-width: 50rem) {
  .figure-thing-actions {
    display: block;
    height: calc(var(--figure-action) * var(--figure-u));
    overflow: hidden;
  }

  .figure-thing-actions .loadout-row-action {
    padding-block: 0;
    line-height: calc((var(--figure-action) - 4) * var(--figure-u));
  }
}

/* The item editor: a panel under the Loadout, held at the foot of the
   layer while it scrolls, a field to a row. */
.seat-item-editor {
  position: sticky;
  bottom: 0;
  display: flex;
  flex-direction: column;
  gap: var(--ui-space-2);
  margin-top: var(--ui-space-3);
  padding: var(--ui-space-3);
  background: var(--ui-surface-raised);
  border: 1px solid var(--ui-edge);
  border-radius: var(--ui-radius-lg);
  box-shadow: var(--ui-elevation-2);
}

.seat-item-editor h3 {
  margin: 0;
  font-size: var(--ui-text-body);
}

.seat-item-field {
  display: grid;
  grid-template-columns: minmax(7rem, auto) minmax(0, 1fr);
  gap: var(--ui-space-1) var(--ui-space-2);
  align-items: center;
}

.seat-item-field .status {
  grid-column: 2;
  margin: 0;
}

.seat-item-field .status:empty {
  display: none;
}

.seat-item-field input[readonly],
.seat-item-field select[aria-disabled="true"] {
  opacity: 0.6;
  cursor: progress;
}

.seat-item-actions {
  display: flex;
  gap: var(--ui-space-2);
}

/* [/PC10] --------------------------------------------------------- */

/* [PC11] the Money view's actions: a row of buttons under each stack
   and the bank, and the form, which takes the item editor's panel. --- */

.money-actions {
  grid-column: 1 / -1;
  display: flex;
  flex-wrap: wrap;
  gap: var(--ui-space-1);
}

.money-bank .money-actions {
  margin-top: var(--ui-space-2);
}

.money-actions button[aria-disabled="true"] {
  opacity: 0.6;
  cursor: progress;
}

.seat-money-form .status {
  margin: 0;
}

.seat-money-form .status:empty {
  display: none;
}

/* [/PC11] --------------------------------------------------------- */

/* The GM's trait library (player-character-sheet PC9): the add form,
   then one row per entry with its card text under it. */
.library-hint {
  margin: 0 0 var(--ui-space-2);
  color: var(--ui-ink-muted);
}

.library-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--ui-space-3);
}

.library-row {
  padding-top: var(--ui-space-2);
  border-top: 1px solid var(--ui-edge);
}

.library-text {
  display: flex;
  flex-direction: column;
}

.library-text textarea {
  width: 100%;
  box-sizing: border-box;
}

.library-kind,
.library-summary {
  color: var(--ui-ink-muted);
}

/* The seat's trait library (player-character-sheet PC9): in Edit, under
   the sheet and held at the foot of the layer while the sheet scrolls,
   so its cards stay in reach of the sections they drop on. */
.seat-library {
  position: sticky;
  bottom: 0;
  z-index: 1;
  display: flex;
  flex-direction: column;
  gap: var(--ui-space-2);
  max-height: 45vh;
  overflow: auto;
  margin-top: var(--ui-space-3);
  padding: var(--ui-space-3);
  background: var(--ui-surface-raised);
  border: 1px solid var(--ui-edge);
  border-radius: var(--ui-radius-lg);
}

.seat-library-title,
.seat-library-heading {
  margin: 0;
}

.seat-library-note {
  margin: 0;
  color: var(--ui-ink-muted);
}

.seat-library-cards {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--ui-space-1);
}

.seat-library-card {
  padding: var(--ui-space-1) var(--ui-space-2);
  border: 1px solid var(--ui-edge);
  border-radius: var(--ui-radius-md);
  background: var(--ui-surface);
  cursor: grab;
}

.seat-library-card-head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--ui-space-2);
}

.seat-library-name {
  font-weight: 600;
}

.seat-library-detail,
.seat-library-text {
  color: var(--ui-ink-muted);
}

.seat-library-card-head button {
  margin-left: auto;
}

.seat-library-text {
  margin: var(--ui-space-1) 0 0;
  white-space: pre-wrap;
}

/* The sheet's drop spots while a card is dragged: outlined, then green
   where the card fits and red where it does not — each with its words
   under it, so the colour is never the only sign. */
.sheet .drop-spot {
  outline: 1px dashed var(--ui-select);
  outline-offset: 2px;
}

.sheet .drop-spot.drop-ok {
  outline: 2px solid var(--ui-accent);
  background: color-mix(in srgb, var(--ui-accent) 22%, transparent);
}

.sheet .drop-spot.drop-no {
  outline: 2px solid var(--ui-error);
  background: color-mix(in srgb, var(--ui-error) 14%, transparent);
}

.sheet-drop-words {
  margin: var(--ui-space-1) 0 0;
  color: var(--ui-ink);
  font-size: 0.875rem;
}

.sheet-drop-words.error {
  color: var(--ui-error);
}

/* [FS6 — one measured save banner (follow-ups sweep item 6)] ------- */

/* MapLibre's NavigationControl hangs from the bar too, at the right, and
   the full-width banner covered it as it covers the View menu. The same
   selector as the rule that places it under the bar, so this one, later
   in the file, wins on order; the dock still does not move. */
.maplibregl-ctrl-top-right {
  top: calc(var(--ui-chrome-top) + var(--save-banner-h));
}

/* [/FS6] ---------------------------------------------------------- */

/* [MC — the map clock (map clock items 3–6)] ------------------------- */

/* The GM's clock widget (map clock item 3), inside the Clock drawer
   (T9, map-view drawers MV0): the drawer's slot, motion and hiding are
   the frame's, in [MV0] at the end of this file, and this is only the
   widget's own column. */
.map-clock {
  display: flex;
  flex-direction: column;
  gap: var(--ui-space-2);
  color: var(--ui-ink);
}

.map-clock-steps {
  display: flex;
  flex-wrap: wrap;
  gap: var(--ui-space-1);
}

.map-clock-steps button {
  font-size: var(--ui-text-small);
  font-variant-numeric: tabular-nums;
}

/* The style switch, a two-button group as the Equipped panel's. */
.clock-style-switch {
  display: inline-flex;
}

.clock-style-switch button {
  border-radius: 0;
  font-size: var(--ui-text-small);
}

.clock-style-switch button:first-child {
  border-radius: var(--ui-radius-md) 0 0 var(--ui-radius-md);
}

.clock-style-switch button:last-child {
  border-left: 0;
  border-radius: 0 var(--ui-radius-md) var(--ui-radius-md) 0;
}

.clock-style-switch button[aria-pressed="true"] {
  background: var(--ui-accent);
  border-color: var(--ui-accent);
}

/* The faces, the GM's and a seat's alike. */
.clock-face {
  display: flex;
  flex-direction: column;
  align-items: center;
}

.clock-digital {
  display: flex;
  flex-direction: column;
  align-items: center;
  margin: 0;
  font-family: var(--ui-font-data);
  font-variant-numeric: tabular-nums;
}

.clock-time {
  font-size: var(--ui-text-display);
  font-weight: 600;
  line-height: 1.1;
}

.clock-date {
  color: var(--ui-ink-muted);
  font-size: var(--ui-text-small);
}

/* Set and Next (map clock item 5): two buttons under the steps, and the
   one panel they open under them. */
.map-clock-more {
  display: flex;
  gap: var(--ui-space-1);
}

.map-clock-more button {
  font-size: var(--ui-text-small);
}

.map-clock-set {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: var(--ui-space-2);
}

.map-clock-set .status:empty {
  display: none;
}

.map-clock-next {
  display: flex;
  flex-direction: column;
  gap: var(--ui-space-1);
  max-width: 18rem;
  margin: 0;
  padding: 0;
  list-style: none;
}

.map-clock-next button {
  display: flex;
  justify-content: space-between;
  gap: var(--ui-space-3);
  width: 100%;
  text-align: left;
}

.next-when {
  color: var(--ui-ink-muted);
  font-family: var(--ui-font-data);
  font-size: var(--ui-text-small);
  font-variant-numeric: tabular-nums;
}

.map-clock-none {
  color: var(--ui-ink-muted);
  font-size: var(--ui-text-small);
}

/* The Time tab's time zone note. */
.clock-zone-note {
  margin: 0;
  color: var(--ui-ink-muted);
  font-size: var(--ui-text-small);
}

/* The traditional dial (map clock item 4). Painted in the token layer,
   so Night and Standard both draw it. A hand's grab line is a wide
   transparent stroke, so a finger finds it; touch-action: none keeps a
   drag on a hand from scrolling or zooming the page. */
.clock-dial {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--ui-space-1);
}

.clock-dial svg {
  width: 8rem;
  height: 8rem;
  overflow: visible;
}

.clock-rim {
  fill: var(--ui-surface-raised);
  stroke: var(--ui-edge);
  stroke-width: 1.5;
}

.clock-tick {
  stroke: var(--ui-ink-muted);
  stroke-width: 1;
  stroke-linecap: round;
}

.clock-tick.long {
  stroke: var(--ui-ink);
  stroke-width: 2;
}

.clock-hand-line {
  stroke: var(--ui-ink);
  stroke-linecap: round;
}

.clock-hand.hour .clock-hand-line {
  stroke-width: 4;
}

.clock-hand.minute .clock-hand-line {
  stroke-width: 2.5;
}

.clock-hand-grab {
  stroke: transparent;
  stroke-width: 12;
  stroke-linecap: round;
}

.clock-hand.live {
  cursor: grab;
  touch-action: none;
}

.clock-dial.dragging .clock-hand.live {
  cursor: grabbing;
}

.clock-hand.live:hover .clock-hand-line,
.clock-hand.live:focus-visible .clock-hand-line,
.clock-dial.dragging .clock-hand-line {
  stroke: var(--ui-select);
}

.clock-hand.live:focus-visible {
  outline: none;
}

.clock-pin {
  fill: var(--ui-ink);
}

.clock-dial-text {
  margin: 0;
  color: var(--ui-ink-muted);
  font-family: var(--ui-font-data);
  font-size: var(--ui-text-small);
  font-variant-numeric: tabular-nums;
}

/* A seat's clock (map clock item 6), in its Clock drawer (map-view
   drawers MV0): the face, then the style switch, the seat's one control
   on it. */
.seat-clock-drawer {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--ui-space-2);
  color: var(--ui-ink);
}

.seat-clock .clock-dial svg {
  width: 6rem;
  height: 6rem;
}

/* [/MC] ----------------------------------------------------------- */

/* [MV0 — the drawer frame (map-view drawers MV0)] ------------------- */

/* A drawer's slot box (drawers.rs): fixed on the chrome's layer, above
   the scrim and the map's overlays and below menus, the palette and the
   save banner. It holds the panel's frame and the tab, laid out per
   slot below so the tab rides the panel's outer edge (decision 6 (a)).
   A surface covering the map covers its drawers too (C7): the GM's data
   overlay through the hide list above. A seat's page has no such
   surface since its tabs became drawers (MV1), so nothing sets
   .covered now, and the Clock's :not(.covered) below matches every
   drawer.
   The box is as wide as the panel and as tall as all it holds, so
   beside a tab narrower than the panel it is empty over the map: it
   takes no click there, and only the panel and the tab take theirs. */
.drawer {
  position: fixed;
  z-index: var(--ui-z-chrome);
  display: flex;
  pointer-events: none;
}

.drawer-fan,
.drawer-tab {
  pointer-events: auto;
}

/* The box the camera measures (data-band), so it never moves and has
   no padding, border or min-size: with the panel in it is empty and
   measures 0 × 0, which is no band. The clip is the slide's: a panel
   coming in from behind the bar is cut at the frame's own edge, and
   everything past the other three — its shadow, and the tab hanging
   outside the frame — still shows. */
.drawer-frame {
  display: flex;
  min-width: 0;
  clip-path: inset(0 -100vw -100vh -100vw);
}

/* The panel and its tab in one box, which is what slides (decision
   6 (a)): the tab rides the panel's edge on the way out instead of
   jumping to where the panel will end. The box is always mounted, so
   its slide is a transition, not a from-state: a slot gives it an
   offset while it holds no panel, and the panel mounting takes the
   offset away. The offset is a percentage of the box's own size, read
   on each frame, so it is a whole panel once the panel is in. Going in,
   the panel goes at once, as every fan's close does, and the tab slides
   back on this same transition from where it stood (drawers.rs's
   keep_slide_back, MV8's E16). The frame around it, which the camera
   measures, never moves. */
.drawer-slide {
  position: relative;
  display: flex;
  min-width: 0;
  transition: transform var(--ui-duration-base) var(--ui-ease-standard);
}

/* The fan, in the frame's flow rather than fixed on its own (the frame
   is its place). .fan has no rule of its own, and .fan-panel is a flex
   child that fills it. */
.drawer-fan {
  display: flex;
  min-width: 0;
}

.drawer-fan .fan-panel {
  border: 1px solid var(--ui-edge);
  box-shadow: var(--ui-elevation-2);
}

.drawer-tab {
  background: var(--ui-surface);
  border: 1px solid var(--ui-edge);
  font-family: var(--ui-font-data);
  font-variant-numeric: tabular-nums;
  box-shadow: var(--ui-elevation-1);
}

/* Where the Clock's slot sits and how wide its panel is, worked out
   on the slot and on each box that keeps clear of it, since
   --ui-chrome-right is the shell's and differs per page:
   --clock-right is the slot's right inset, --clock-room the width the
   panel may take, and --clock-left the inset of the panel's left edge
   from the window's right, where a box beside it has to stop. */
.drawer[data-slot="top-clock"],
.drawer[data-slot="top"],
.map-status-stack,
.player-status {
  --clock-right: calc(var(--ui-chrome-right) + 3rem);
  --clock-room: calc(100vw - var(--clock-right) - var(--ui-space-2));
  --clock-width: min(17rem, var(--clock-room));
  --clock-left: calc(var(--clock-right) + var(--clock-width));
}

/* On the GM's page the panel stops short of the map tools at home
   (.map-tools[data-home]: 9.5rem wide at the space-3 inset), which are
   on the same layer and mounted before it, so it would cover them and
   take their clicks. It keeps the dial's width at the least, which
   still clears them at 375 px; with the dock open between 641 and about
   730 px there is no room for both, and the panel drops below them
   instead (the narrow rules below). The top slot, which stops left of
   the Clock, works it out the same way. */
.shell:has(.map-tools[data-home]) :is(.drawer[data-slot="top-clock"], .drawer[data-slot="top"], .map-status-stack) {
  --clock-room: max(10rem, calc(100vw - var(--clock-right) - var(--ui-space-3) - 9.5rem - var(--ui-space-2)));
}

/* From 960 px up (under it, the narrow rules below), while the Clock
   is out the map's status stack keeps to the room left of its panel,
   centred there; the stack's own padding is the gap. It is on the
   map's overlay layer, under the Clock, which otherwise hid a long
   notice's line ends. The 16rem floor holds at every width here. */
@media (min-width: 960px) {
  :root:has(.drawer[data-slot="top-clock"]:not(.covered) .drawer-fan) .map-status-stack {
    right: min(var(--clock-left), calc(100% - 16rem - 2 * var(--ui-space-4)));
  }

  /* …and so does a seat's status line, top left on the chrome's layer
     and mounted after the Clock, which "Reconnecting…" was printed
     across. Its 16rem floor holds here too. */
  :root:has(.drawer[data-slot="top-clock"]:not(.covered) .drawer-fan) .player-status {
    max-width: min(28rem, calc(100vw - 2 * var(--ui-space-3)), max(16rem, calc(100vw - var(--clock-left) - var(--ui-space-3) - var(--ui-space-2))));
  }
}

/* The Clock's slot (C1): just left of MapLibre's zoom and compass,
   under the bar and the save banner, stepping aside from the GM's open
   dock and a seat's panel as those controls do (--ui-chrome-right).
   It hangs down: the panel, then its tab under it, out of the slide's
   flow so the frame measures the panel alone. */
.drawer[data-slot="top-clock"] {
  top: calc(var(--ui-chrome-top) + var(--save-banner-h));
  right: var(--clock-right);
  flex-direction: column;
  align-items: flex-end;
}

.drawer[data-slot="top-clock"] .drawer-fan {
  width: var(--clock-width);
  max-height: calc(100dvh - var(--ui-chrome-top) - var(--save-banner-h) - 4rem);
}

.drawer[data-slot="top-clock"] .fan-panel {
  border-top: 0;
  border-radius: 0 0 0 var(--ui-radius-lg);
}

.drawer[data-slot="top-clock"] .drawer-tab {
  position: absolute;
  top: 100%;
  right: 0;
  white-space: nowrap;
  border-top: 0;
  border-radius: 0 0 var(--ui-radius-md) var(--ui-radius-md);
}

/* In, the box is up by its own height, which is nothing while it holds
   only the tab; out, it slides down from behind the bar. */
.drawer[data-slot="top-clock"] .drawer-slide:not(:has(> .drawer-fan)) {
  transform: translateY(-100%);
}

/* A group's tabs (DrawerGroup): one row or column riding the open
   panel's outer edge, out of the slide's flow as the Clock's tab is, so
   the frame measures the panel alone. Each slot's region places it. */
.drawer-tabs {
  position: absolute;
  display: flex;
  gap: var(--ui-space-1);
}

/* The bottom slot (decision S of wave B): centred left of the
   right-hand chrome, the tab on the panel's top edge. The frame's clip
   is turned over, so the slide comes up from the window's foot and the
   tab hanging above the frame still shows. While in, the box that
   slides sits below the foot by its own height, which is nothing while
   it holds only the tab. A seat's tray (MV1) and the GM's map tools
   (MV6) both sit here, on different pages (and View as… mounts a seat's
   inside the GM's shell), so each keys its own rules off the box's
   data-scope in its region, never off the slot alone.
   How far up it reaches is --drawer-bottom-reach (drawers.rs). */
.drawer[data-slot="bottom"] {
  left: 0;
  right: var(--ui-chrome-right);
  bottom: 0;
  flex-direction: column;
  align-items: center;
}

.drawer[data-slot="bottom"] .drawer-frame {
  clip-path: inset(-100vh -100vw 0 -100vw);
}

.drawer[data-slot="bottom"] .fan-panel {
  border-bottom: 0;
  border-radius: var(--ui-radius-lg) var(--ui-radius-lg) 0 0;
}

.drawer[data-slot="bottom"] .drawer-slide > .drawer-tab {
  position: absolute;
  bottom: 100%;
  left: 50%;
  translate: -50% 0;
  white-space: nowrap;
  border-bottom: 0;
  border-radius: var(--ui-radius-md) var(--ui-radius-md) 0 0;
}

.drawer[data-slot="bottom"] .drawer-slide:not(:has(> .drawer-fan)) {
  transform: translateY(100%);
}

/* At a phone's width a seat's panel publishes 22rem of the window, so
   the slot falls back to the corner rather than leave the screen. */
@media (max-width: 640px) {
  .drawer[data-slot="top-clock"],
  .drawer[data-slot="top"],
  .map-status-stack,
  .player-status {
    --clock-right: 3rem;
  }
}

/* Under 960 px (drawers::NARROW, C9) no room fits the open Clock
   beside a seat's status line, the map's notices and, with the dock
   open, the GM's map tools. So the Clock starts in there (drawers.rs), and
   its whole slot, tab and all, drops below whichever of them it would
   cover, by the --drawer-drop drawers.rs measures; the panel's height
   gives that back, so it still ends on screen. */
@media (max-width: 959.98px) {
  .drawer[data-slot="top-clock"] {
    top: calc(var(--ui-chrome-top) + var(--save-banner-h) + var(--drawer-drop, 0px));
  }

  .drawer[data-slot="top-clock"] .drawer-fan {
    max-height: calc(100dvh - var(--ui-chrome-top) - var(--save-banner-h) - var(--drawer-drop, 0px) - 4rem);
  }
}

/* The drawers' one stacking layer (drawers.rs's DrawerLayer, the call
   of 2026-10-03). A fixed box makes its own stacking context, and so do
   a clip and a transform, so a drawer standing alone paints whole, tab
   and panel, above or below another. Inside the layer the slot box is
   absolute over a window-sized box and the frame has no clip, so every
   panel and tab paints in the layer's one context: a panel by its
   --drawer-rank, the drawer out last in front where they meet, and
   every tab, and a slide holding only its tab, above every panel. The
   layer cuts what slides at the bar's foot, as each frame's clip did at
   its own edge; the window's three other edges cut the rest. It takes
   no click itself; what it holds besides drawers takes its own. */
.drawer-layer {
  position: fixed;
  inset: 0;
  z-index: var(--ui-z-chrome);
  pointer-events: none;
  clip-path: inset(calc(var(--ui-chrome-top) + var(--save-banner-h)) 0 0 0);
}

.drawer-layer > .drawer {
  position: absolute;
  z-index: auto;
}

.drawer-layer > :not(.drawer) {
  pointer-events: auto;
}

.drawer-layer .drawer[data-slot] .drawer-frame {
  clip-path: none;
}

.drawer-layer .drawer-fan {
  position: relative;
  z-index: var(--drawer-rank, 0);
}

.drawer-layer :is(.drawer-tab, .drawer-tabs),
.drawer-layer .drawer-slide:not(:has(> .drawer-fan)) {
  z-index: 10;
}

/* [/MV0] ---------------------------------------------------------- */

/* [MV1 — a seat's left edge and Characters tray (map-view drawers MV1)] */

/* The offer card in the drawers' layer (player_shell.rs mounts it
   last inside it): over every panel, whatever its rank, and under every
   tab, so where its 3rem floor reaches a tab row the tab paints and
   takes the click over the card (the rewalk at 375 px). */
.drawer-layer > .offer-stack {
  z-index: 9;
}

/* The left slot (L1, L5): under the bar and the save banner, and below
   a showing status line or notice by the --drawer-drop drawers.rs
   measures for it at every width. Its panel is 24rem, never wider than
   the window less its tab column, and slides in from the window's left
   edge, where the frame's clip cuts it; past the other three edges its
   shadow and its tabs still show. */
.drawer[data-slot="left"] {
  top: calc(var(--ui-chrome-top) + var(--save-banner-h) + var(--drawer-drop, 0px));
  left: 0;
  flex-direction: row;
  align-items: flex-start;
}

.drawer[data-slot="left"] .drawer-frame {
  clip-path: inset(-100vh -100vw -100vh 0);
}

.drawer[data-slot="left"] .drawer-fan {
  width: min(24rem, calc(100vw - 3rem));
  max-height: calc(100dvh - var(--ui-chrome-top) - var(--save-banner-h) - var(--drawer-drop, 0px) - var(--drawer-bottom-reach, 0px) - var(--ui-space-4));
}

.drawer[data-slot="left"] .fan-panel {
  border-left: 0;
  border-radius: 0 0 var(--ui-radius-lg) 0;
  overscroll-behavior: contain;
}

/* In, the box is left by its own width, which is nothing while it holds
   only the tabs; out, it slides in from the window's edge. */
.drawer[data-slot="left"] .drawer-slide:not(:has(> .drawer-fan)) {
  transform: translateX(-100%);
}

/* The tabs: a column on the panel's right edge, read top to bottom. */
.drawer[data-slot="left"] .drawer-tabs {
  top: 0;
  left: 100%;
  flex-direction: column;
}

.drawer[data-slot="left"] .drawer-tab {
  writing-mode: vertical-rl;
  padding: var(--ui-space-3) var(--ui-space-1);
  white-space: nowrap;
  border-left: 0;
  border-radius: 0 var(--ui-radius-md) var(--ui-radius-md) 0;
}

.drawer[data-slot="left"] .drawer-tab[aria-expanded="true"] {
  background: var(--ui-surface-raised);
  box-shadow: inset 2px 0 0 var(--ui-accent), var(--ui-elevation-1);
}

/* The library sticks to the foot of the Character drawer's panel,
   which the bar, the tray and its gaps leave shorter than the old
   full-height layer: at 45vh it would leave about 230 px of sheet to
   drop onto at 800 px tall, so on a short window it keeps 30vh. */
@media (max-height: 900px) {
  .drawer[data-slot="left"] .seat-library {
    max-height: 30vh;
  }
}

/* The compact sheet (L1), in the left drawer only: sheet_ui is the GM's
   card's too, so nothing here reaches past the drawer. Less padding and
   gap, the trackers two a row with smaller boxes, and one column of
   traits, which the panel's width makes of the sheet's own grid. */
.drawer[data-slot="left"] .seat-sheet-card {
  padding: var(--ui-space-2) var(--ui-space-3);
}

.drawer[data-slot="left"] .sheet {
  gap: var(--ui-space-2);
  margin-top: var(--ui-space-2);
}

.drawer[data-slot="left"] .sheet-tracks {
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--ui-space-2);
}

.drawer[data-slot="left"] :is(.sheet-track, .sheet-column, .sheet-block) {
  padding: var(--ui-space-2);
}

.drawer[data-slot="left"] .sheet-box {
  width: 1.25rem;
  height: 1.25rem;
}

.drawer[data-slot="left"] .sheet-box.sheet-humanity {
  width: auto;
}

.drawer[data-slot="left"] :is(.sheet-columns, .sheet-advantage-groups) {
  grid-template-columns: minmax(0, 1fr);
  gap: var(--ui-space-2);
}

.drawer[data-slot="left"] .sheet-trait {
  min-height: 1.5rem;
}

/* The Characters tray (19 (c), L3): the bottom slot's panel as one
   row, the chips first and the fan's Close last, its heading kept for
   focus and a screen reader but not drawn — the tab already says
   "Characters". Keyed off the box's data-scope, since the GM's map
   tools (MV6) share the bottom slot. */
.drawer[data-scope^="tray-"] .drawer-fan {
  max-width: calc(100vw - var(--ui-chrome-right) - 2 * var(--ui-space-3));
}

.drawer[data-scope^="tray-"] .fan-panel {
  flex-direction: row;
  align-items: center;
  gap: var(--ui-space-3);
  padding: var(--ui-space-2) var(--ui-space-3);
}

.drawer[data-scope^="tray-"] .fan-head {
  order: 1;
}

.drawer[data-scope^="tray-"] .fan-head h3 {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* One row of chips that scrolls sideways rather than wrapping, in the
   seat's own order. The padding keeps a focus ring and the scrollbar
   off the chips. */
.seat-tray-chips {
  display: flex;
  flex-wrap: nowrap;
  gap: var(--ui-space-2);
  min-width: 0;
  overflow-x: auto;
  overscroll-behavior-x: contain;
  scrollbar-width: thin;
  padding: 0.125rem;
}

/* A chip: the pick, and beside it the ★ that plays as it, in one pill. */
.seat-chip {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: stretch;
  border: 1px solid var(--ui-edge);
  border-radius: 999px;
  background: var(--ui-surface);
  white-space: nowrap;
}

.seat-chip.viewed {
  border-color: var(--ui-accent);
  background: var(--ui-surface-raised);
  box-shadow: inset 0 0 0 1px var(--ui-accent);
}

.seat-chip-pick,
.seat-chip-only {
  display: inline-flex;
  align-items: center;
  gap: var(--ui-space-2);
  min-height: 2.25rem;
  padding: var(--ui-space-1) var(--ui-space-4);
}

.seat-chip-pick,
.seat-chip-play {
  background: none;
  border: 0;
  border-radius: 999px;
}

.seat-chip-only {
  margin: 0;
}

.seat-chip-play {
  padding: 0 var(--ui-space-3) 0 var(--ui-space-1);
  color: var(--ui-ink-muted);
}

.seat-chip-play:hover,
.seat-chip-play:focus-visible {
  color: var(--ui-select);
}

/* A ★ whose Play as is still out (PC8): aria-disabled rather than
   disabled, so it keeps the focus it was pressed with. */
.seat-chip-play[aria-disabled="true"] {
  opacity: 0.5;
  cursor: progress;
}

.seat-chip-name {
  font-weight: 600;
}

.seat-chip-kind {
  color: var(--ui-ink-muted);
  font-size: var(--ui-text-caption);
}

/* Who the seat is acting as: the canvas's gold, which is the selection
   slot's. The chip also says "(primary)" to a screen reader, so the
   star is never the only sign. */
.seat-chip-star {
  color: var(--ui-select);
}

.seat-tray-none {
  margin: 0;
  color: var(--ui-ink-muted);
}

/* What the ★'s Play as was answered, just above the tray, in or out,
   and gone while it has nothing to say. It centres on the tray's span,
   the window less the seat's panel, and keeps to two lines, which the
   offer cards rise above while it speaks: on the same layer and
   mounted later, they would lie over it. Its own class, since the
   proposals' line in the Character drawer is a .seat-tray-status. */
.seat-tray-answer {
  position: fixed;
  left: calc((100vw - var(--ui-chrome-right)) / 2);
  bottom: calc(var(--drawer-bottom-reach, 0px) + var(--ui-space-1));
  translate: -50% 0;
  z-index: var(--ui-z-chrome);
  max-width: calc(100vw - var(--ui-chrome-right) - 2 * var(--ui-space-3));
  margin: 0;
  padding: var(--ui-space-1) var(--ui-space-3);
  line-height: 1.4;
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  overflow: hidden;
  background: var(--ui-surface);
  border: 1px solid var(--ui-edge);
  border-radius: var(--ui-radius-md);
  box-shadow: var(--ui-elevation-1);
}

.seat-tray-answer.silent {
  display: none;
}

.player-shell:has(.seat-tray-answer:not(.silent)) .offer-stack {
  --seat-tray-answer-room: calc(2 * 1.4 * var(--ui-text-small) + 2 * var(--ui-space-1) + 2px);
  bottom: calc(var(--ui-space-3) + var(--drawer-bottom-reach, 0px) + var(--seat-tray-answer-room));
  --offer-max: calc(100vh - var(--ui-chrome-top) - var(--drawer-bottom-reach, 0px) - var(--seat-tray-answer-room) - 4 * var(--ui-space-3));
}

/* MapLibre's bottom-right control, the in-canvas map credit, rises
   above the tray on a seat's page and in View as…: unabridged, it runs
   across most of the map's foot, so no tray could stop short of it
   beside it, and the credit has to stay readable. */
:is(.player-shell, .player-map) .maplibregl-ctrl-bottom-right {
  bottom: var(--drawer-bottom-reach, 0px);
}

/* From 960 px up, View as…'s notices keep left of the seat's panel on
   the right, whose width the shell publishes; with the Clock out they
   keep left of it instead ([MV0]). Under 960 px the panel's width would
   leave them no room, and the left drawer drops below them (L5). */
@media (min-width: 960px) {
  .shell.seated:has(.player-panel) .map-status-stack {
    right: var(--ui-chrome-right);
  }
}

/* An open Messages, Notes, Requests or page fan (.seat-talk, mounted
   only while open) is a wall from the bar to the window's foot, on the
   drawers' layer and painted before them. Under 768 px the left panel,
   out, would reach under it, so it steps out while the fan is open, as
   the offer stack does on a phone, and comes back as it shuts, as it
   was: display: none, so its controls leave the tab order too. */
@media (max-width: 767.98px) {
  .player-shell:has(.seat-talk) .drawer[data-slot="left"]:has(.drawer-fan) {
    display: none;
  }
}

/* Phones (L6, 16 (a)): the left panel takes the window's width less its
   tab column, and the tray centres on the window, the seat's panel
   publishing more of the width than a phone has. The chips are ones a
   thumb can hit. */
@media (max-width: 640px) {
  .drawer[data-slot="left"] .drawer-fan {
    width: calc(100vw - 2.5rem);
  }

  .drawer[data-scope^="tray-"] {
    right: 0;
  }

  .drawer[data-scope^="tray-"] .drawer-fan {
    max-width: calc(100vw - 2 * var(--ui-space-2));
  }

  .seat-tray-answer {
    left: 50%;
    max-width: calc(100vw - 2 * var(--ui-space-2));
  }

  .player-shell:has(.seat-tray-answer:not(.silent)) .offer-stack {
    bottom: calc(var(--ui-space-2) + var(--drawer-bottom-reach, 0px) + var(--seat-tray-answer-room));
    --offer-max: min(55vh, calc(100vh - var(--ui-chrome-top) - var(--drawer-bottom-reach, 0px) - var(--seat-tray-answer-room) - 4 * var(--ui-space-2)));
  }

  /* The offer card steps aside from the left drawers' tab column (the
     join's call): it stays over the drawers, mounted after them, but
     never over their tabs. The column is the 2.5rem the panel leaves
     above: at the window's left edge while the panel is in, and past
     the panel, at its right edge, while one is out. */
  .player-shell .offer-stack {
    left: calc(2.5rem + var(--ui-space-1));
  }

  .player-shell:has(.drawer[data-slot="left"] .drawer-fan) .offer-stack {
    left: var(--ui-space-2);
    right: calc(2.5rem + var(--ui-space-1));
  }

  .seat-chip-pick,
  .seat-chip-only,
  .seat-chip-play {
    min-height: 2.75rem;
  }

  /* On a phone the fan takes the whole width, so the tray, its answer
     and the left edge's tabs step out with the left panel. */
  .player-shell:has(.seat-talk) :is(.drawer[data-scope^="tray-"], .drawer[data-slot="left"], .seat-tray-answer) {
    display: none;
  }
}

/* [/MV1] ---------------------------------------------------------- */

/* [MV4 — Party and Contacts along the top (map-view drawers MV4)] ---- */

/* The top slot (U7): right-anchored left of the Clock, hanging down
   like it, the panel and then its row of tabs under it, out of the
   slide's flow so the frame measures the panel alone. --top-right is
   its right inset: while the Clock is in, left of the Clock's tab (its
   HH:MM, about 4.5rem wide, so 5.5rem leaves a margin); while the Clock
   is out, left of its panel from 960 px up, and under 960 px the
   Clock's own inset, so the drawer shares the Clock's span and drops
   below its panel and tab (drawers.rs's clear_of). It drops below the
   page's status line and the map's notices at every width (U8), by the
   --drawer-drop drawers.rs measures. */
.drawer[data-slot="top"] {
  --top-right: calc(var(--clock-right) + 5.5rem + var(--ui-space-2));
  top: calc(var(--ui-chrome-top) + var(--save-banner-h) + var(--drawer-drop, 0px));
  right: var(--top-right);
  flex-direction: column;
  align-items: flex-end;
}

@media (min-width: 960px) {
  :root:has(.drawer[data-slot="top-clock"]:not(.covered) .drawer-fan) .drawer[data-slot="top"] {
    --top-right: calc(var(--clock-left) + var(--ui-space-2));
  }
}

@media (max-width: 959.98px) {
  :root:has(.drawer[data-slot="top-clock"]:not(.covered) .drawer-fan) .drawer[data-slot="top"] {
    --top-right: var(--clock-right);
  }
}

/* Up to 20rem, never past the window's left edge; up to 28rem tall,
   never past its foot, the drop given back, nor down over a bottom
   drawer (a seat's tray, the GM's map tools), which mounts before it on
   the same layer: as the left panel does, it stops short of how far up
   that reaches. */
.drawer[data-slot="top"] .drawer-fan {
  width: min(20rem, calc(100vw - var(--top-right) - var(--ui-space-2)));
  max-height: min(28rem, calc(100dvh - var(--ui-chrome-top) - var(--save-banner-h) - var(--drawer-drop, 0px) - var(--drawer-bottom-reach, 0px) - 4rem));
}

.drawer[data-slot="top"] .fan-panel {
  border-top: 0;
  border-radius: 0 0 var(--ui-radius-lg) var(--ui-radius-lg);
}

.drawer[data-slot="top"] .drawer-tabs {
  top: 100%;
  right: 0;
}

.drawer[data-slot="top"] .drawer-tab {
  white-space: nowrap;
  border-top: 0;
  border-radius: 0 0 var(--ui-radius-md) var(--ui-radius-md);
}

/* The open drawer's tab reads as pressed. */
.drawer[data-slot="top"] .drawer-tab[aria-expanded="true"] {
  border-color: var(--ui-accent);
}

/* In, the box is up by its own height, which is nothing while it holds
   only the tabs; out, it slides down from behind the bar. */
.drawer[data-slot="top"] .drawer-slide:not(:has(> .drawer-fan)) {
  transform: translateY(-100%);
}

/* On a phone an open Messages, Notes, Requests or page fan
   (.seat-talk, mounted only while open) takes the whole width, on the
   drawers' layer and painted before them, so a seat's top drawers and
   its Clock, which reach across it, step out while it is open and come
   back as it shuts, as the left edge and the tray do ([MV1]). Party's
   Page opens the page fan, which would otherwise open under the Party
   panel, with the Clock's tab over its Close (the rewalk at 375 px). */
@media (max-width: 640px) {
  .player-shell:has(.seat-talk) .drawer[data-slot="top"],
  .player-shell:has(.seat-talk) .drawer[data-slot="top-clock"] {
    display: none;
  }
}

/* A row's star, beside its name. */
.player-list .party-star {
  margin-left: var(--ui-space-1);
  color: var(--ui-accent);
}

/* The GM's rows' doors: View as… and Open card, side by side. */
.player-list .top-door {
  margin: var(--ui-space-1) var(--ui-space-1) 0 0;
  font-size: var(--ui-text-small);
}

/* The GM's Party: a card per seat, its label the heading, "off" on a
   seat switched off. */
.party-card + .party-card {
  margin-top: var(--ui-space-3);
}

.party-card h3 {
  margin: 0;
}

.party-card.off h3 {
  color: var(--ui-ink-muted);
}

.party-card .seat-off {
  font-size: var(--ui-text-small);
  font-weight: normal;
}

/* [/MV4] ---------------------------------------------------------- */

/* [MV6 — the GM's map tools box and bottom drawer (map-view drawers MV6)] */

/* The map tools in the bottom drawer (P7–P9), the View menu's other
   pick: the toolbar is a row there, its groups side by side and parted
   by a rule, wrapping on a narrow window rather than leaving it. The
   drawer is never the box (C3): these key off the slot's scope. */
.drawer[data-scope="map-tools"] .drawer-fan {
  max-width: calc(100vw - var(--ui-chrome-right) - 2 * var(--ui-space-3));
}

.drawer[data-scope="map-tools"] .map-toolbar {
  flex-direction: row;
  flex-wrap: wrap;
  justify-content: center;
}

.drawer[data-scope="map-tools"] .map-tools-group {
  flex-direction: row;
  flex-wrap: wrap;
}

.drawer[data-scope="map-tools"] .map-tools-group + .map-tools-group {
  border-left: 1px solid var(--ui-edge);
  padding-left: var(--ui-space-3);
}

.drawer[data-scope="map-tools"] .map-toolbar button {
  width: auto;
}

/* MapLibre's attribution, which discharges the ODbL credit, and its
   supplement on the left rise over the drawer by how far up it reaches,
   as the hint strip does (P9): the panel covered them at 1280 px and
   below (review). */
:root:has(.drawer[data-scope="map-tools"]) .maplibregl-ctrl-bottom-right {
  bottom: var(--drawer-bottom-reach, 0px);
}

:root:has(.drawer[data-scope="map-tools"]) .map-attribution {
  bottom: calc(var(--ui-space-2) + var(--drawer-bottom-reach, 0px));
}

/* The View menu's switch between the two, the same step group as the
   render distance above it. */
.view-menu .map-tools-mode > span {
  color: var(--ui-ink-muted);
}

.view-menu .map-tools-mode {
  margin: 0;
}

/* [/MV6] ---------------------------------------------------------- */

/* [MV2 — a seat's right edge: Car and Nearby (map-view drawers MV2)] - */

/* The right slot (R1): at the window's right edge, over the seat's
   panel, under the bar and the save banner, and below the Clock or the
   top drawers by the --drawer-drop drawers.rs measures for it at every
   width (seat_right.rs). Its panel is the panel's width, never wider
   than the window less its tab column, and slides in from the window's
   right edge, where the frame's clip cuts it; past the other three
   edges its shadow and its tabs still show. */
.drawer[data-slot="right"] {
  top: calc(var(--ui-chrome-top) + var(--save-banner-h) + var(--drawer-drop, 0px));
  right: 0;
  flex-direction: row-reverse;
  align-items: flex-start;
}

.drawer[data-slot="right"] .drawer-frame {
  clip-path: inset(-100vh 0 -100vh -100vw);
}

.drawer[data-slot="right"] .drawer-fan {
  width: min(var(--ui-dock-width), calc(100vw - 3rem));
  max-height: calc(100dvh - var(--ui-chrome-top) - var(--save-banner-h) - var(--drawer-drop, 0px) - var(--drawer-bottom-reach, 0px) - 4rem);
}

.drawer[data-slot="right"] .fan-panel {
  border-right: 0;
  border-radius: 0 0 0 var(--ui-radius-lg);
  overscroll-behavior: contain;
}

/* In, the box is right by its own width, which is nothing while it
   holds only the tabs; out, it slides in from the window's edge. */
.drawer[data-slot="right"] .drawer-slide:not(:has(> .drawer-fan)) {
  transform: translateX(100%);
}

/* The tabs: a column on the panel's left edge, read top to bottom. */
.drawer[data-slot="right"] .drawer-tabs {
  top: 0;
  right: 100%;
  flex-direction: column;
}

.drawer[data-slot="right"] .drawer-tab {
  writing-mode: vertical-rl;
  padding: var(--ui-space-3) var(--ui-space-1);
  white-space: nowrap;
  border-right: 0;
  border-radius: var(--ui-radius-md) 0 0 var(--ui-radius-md);
}

.drawer[data-slot="right"] .drawer-tab[aria-expanded="true"] {
  background: var(--ui-surface-raised);
  box-shadow: inset -2px 0 0 var(--ui-accent), var(--ui-elevation-1);
}

/* A pane: the trunk or what is within reach, then its move's answer;
   the cards as compact as the left drawer's Inventory draws them. */
.seat-right {
  display: flex;
  flex-direction: column;
  gap: var(--ui-space-3);
  min-width: 0;
}

.seat-right .status {
  margin: 0;
}

.drawer[data-slot="right"] .loadout-card {
  margin-inline-start: calc(var(--loadout-depth, 0) * var(--ui-space-2));
  padding: var(--ui-space-2);
}

.drawer[data-slot="right"] .loadout-meter {
  grid-template-columns: 3.5rem minmax(2rem, 1fr) auto;
}

.drawer[data-slot="right"] .loadout-item {
  grid-template-columns: minmax(0, 1fr) auto 2rem 3.25rem;
  gap: var(--ui-space-1);
}

.drawer[data-slot="right"] .loadout-rider:not(.counted) {
  grid-template-columns: minmax(0, 1fr);
}

.seat-right [draggable="true"] {
  cursor: grab;
}

/* While a drag is on, every card that may take a drop is outlined, as
   the Inventory's are. */
.seat-right.dragging .loadout-card {
  outline: 1px dashed var(--ui-select);
  outline-offset: 2px;
}

/* The right drawers and the Messages, Notes, Requests and page fans
   (.seat-talk, mounted only while open) take turns on the right wall
   (R2, player_shell.rs): while a fan is open the right drawer is in, and
   its tabs step out to the fan's left edge, beside it rather than over
   its panel. Under 768 px the fan leaves the map too little room beside
   it, so the right edge steps out while the fan is open, as the left
   panel does ([MV1]), and comes back as it shuts. */
.player-shell:has(.seat-talk) .drawer[data-slot="right"] {
  right: min(var(--ui-dock-width), 100vw);
}

@media (max-width: 767.98px) {
  .player-shell:has(.seat-talk) .drawer[data-slot="right"] {
    display: none;
  }
}

/* [/MV2] ---------------------------------------------------------- */

/* [MV3 — the three left groups (map-view drawers MV3)] -------------- */

/* The left's three groups in one column (R10): Character and Inventory
   at the top, where [MV1] places it, then Notes & details, then Merits
   & Disciplines. drawers.rs's keep_left_stacked writes, on each box,
   --stack-top where the two lower groups sit, and on a group whose
   panel is out --stack-min, its tab column's height, and --stack-max
   while the column is too short for all it asks; so all three can be
   out at once and none overlaps another, an 800 px tall window
   included. Until it has measured, the lower two sit halfway down and
   on the foot. */
.drawer:is([data-slot="left-middle"], [data-slot="left-bottom"]) {
  top: var(--stack-top, 50vh);
  left: 0;
  flex-direction: row;
  align-items: flex-start;
}

.drawer[data-slot="left-bottom"] {
  top: var(--stack-top, calc(100dvh - var(--drawer-bottom-reach, 0px) - var(--ui-space-4)));
}

.drawer:is([data-slot="left-middle"], [data-slot="left-bottom"]) .drawer-frame {
  clip-path: inset(-100vh -100vw -100vh 0);
}

/* A panel is never shorter than its tabs, which ride its edge, nor
   taller than its share of the column; the top group keeps [MV1]'s
   limit under that. */
.drawer[data-slot^="left"] .drawer-fan {
  min-height: var(--stack-min, 0px);
}

.drawer[data-slot="left"] .drawer-fan {
  max-height: min(var(--stack-max, 100dvh), calc(100dvh - var(--ui-chrome-top) - var(--save-banner-h) - var(--drawer-drop, 0px) - var(--drawer-bottom-reach, 0px) - var(--ui-space-4)));
}

.drawer:is([data-slot="left-middle"], [data-slot="left-bottom"]) .drawer-fan {
  width: min(24rem, calc(100vw - 3rem));
  max-height: var(--stack-max, calc(100dvh - var(--stack-top, 50vh) - var(--drawer-bottom-reach, 0px) - var(--ui-space-4)));
}

.drawer:is([data-slot="left-middle"], [data-slot="left-bottom"]) .fan-panel {
  border-left: 0;
  border-radius: 0 var(--ui-radius-lg) var(--ui-radius-lg) 0;
  overscroll-behavior: contain;
}

/* In, the box is left by its own width, as the top group's is. */
.drawer:is([data-slot="left-middle"], [data-slot="left-bottom"]) .drawer-slide:not(:has(> .drawer-fan)) {
  transform: translateX(-100%);
}

/* The tabs: a column on the panel's right edge, as the top group's, at
   its top; the bottom group's at its foot, so they stay home on the
   column's foot as its panel rises, and hang above the box while in. */
.drawer:is([data-slot="left-middle"], [data-slot="left-bottom"]) .drawer-tabs {
  top: 0;
  left: 100%;
  flex-direction: column;
}

.drawer[data-slot="left-bottom"] .drawer-tabs {
  top: auto;
  bottom: 0;
}

.drawer:is([data-slot="left-middle"], [data-slot="left-bottom"]) .drawer-tab {
  writing-mode: vertical-rl;
  white-space: nowrap;
  border-left: 0;
  border-radius: 0 var(--ui-radius-md) var(--ui-radius-md) 0;
}

/* Six tabs share the column, so every left tab is a little shorter
   than [MV1]'s one group had them, and under 960 px tall smaller still:
   with the top group dropped below the open Clock or the top drawers,
   or on a phone, the three columns fit the room left
   (drawers::stack_left). */
.drawer[data-slot^="left"] .drawer-tab {
  padding: var(--ui-space-2) var(--ui-space-1);
}

@media (max-height: 959.98px) {
  .drawer[data-slot^="left"] .drawer-tab {
    padding: var(--ui-space-1);
    font-size: var(--ui-text-caption);
  }
}

.drawer:is([data-slot="left-middle"], [data-slot="left-bottom"]) .drawer-tab[aria-expanded="true"] {
  background: var(--ui-surface-raised);
  box-shadow: inset 2px 0 0 var(--ui-accent), var(--ui-elevation-1);
}

/* An open Messages, Notes, Requests or page fan keeps its wall: under
   768 px an out lower panel steps out while it is open, as the top
   group's does ([MV1]). */
@media (max-width: 767.98px) {
  .player-shell:has(.seat-talk) .drawer:is([data-slot="left-middle"], [data-slot="left-bottom"]):has(.drawer-fan) {
    display: none;
  }
}

/* Phones (16 (a)): a lower panel takes the width less its tab column,
   as the top group's does; the offer card steps aside from the tabs
   while one is out; and an open fan puts the lower groups out of the
   way, tabs and all, as it does the top group. */
@media (max-width: 640px) {
  .drawer:is([data-slot="left-middle"], [data-slot="left-bottom"]) .drawer-fan {
    width: calc(100vw - 2.5rem);
  }

  .player-shell:has(.drawer:is([data-slot="left-middle"], [data-slot="left-bottom"]) .drawer-fan) .offer-stack {
    left: var(--ui-space-2);
    right: calc(2.5rem + var(--ui-space-1));
  }

  .player-shell:has(.seat-talk) .drawer:is([data-slot="left-middle"], [data-slot="left-bottom"]) {
    display: none;
  }
}

/* A lower pane (seat_details.rs): one column, the sheet's View and
   Edit at its head where the seat proposes. */
.seat-details {
  display: flex;
  flex-direction: column;
  gap: var(--ui-space-3);
  min-width: 0;
}

.seat-details > .sheet-mode {
  align-self: flex-start;
}

.seat-details-none,
.seat-details .seat-sheet-note {
  margin: 0;
  color: var(--ui-ink-muted);
}

/* Details (D1): the four facts as the sheet's text fields, one under
   another, then the touchstones, each with its conviction beside it. */
.seat-details-facts {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--ui-space-2);
}

.seat-details-facts .sheet-track {
  padding: var(--ui-space-1) var(--ui-space-2);
  border-radius: var(--ui-radius-md);
}

.seat-details-facts .sheet-track-head {
  justify-content: flex-start;
}

.seat-touchstones-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--ui-space-1);
}

.seat-touchstones-list li,
.seat-touchstone-add {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  gap: var(--ui-space-2);
  align-items: baseline;
}

.seat-touchstones-list.editing li,
.seat-touchstone-add {
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) auto;
  align-items: center;
  margin-top: var(--ui-space-1);
}

.seat-conviction {
  color: var(--ui-ink-muted);
}

/* A proposal on a Details field, painted as the sheet's rows are (PC7):
   the word beside it says which. */
.seat-details .pending {
  background: color-mix(in srgb, var(--ui-pending) 12%, transparent);
  outline: 1px solid color-mix(in srgb, var(--ui-pending) 55%, transparent);
  outline-offset: 1px;
}

.seat-details .drafted {
  outline: 1px dashed var(--ui-pending);
  outline-offset: 1px;
}

/* What waits in the tray, while the Character drawer that sends it is
   in, and the way to it. */
.seat-details-tray {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--ui-space-2);
  margin: 0;
  color: var(--ui-pending);
}

/* Notes (R13): the character's notes, newest first, then a note to add,
   in the Notes fan's own look. */
.seat-details-notes .note-head {
  display: flex;
  justify-content: space-between;
  gap: var(--ui-space-2);
  margin: 0 0 var(--ui-space-1);
}

.seat-details-notes .talk-body {
  margin: 0;
  white-space: pre-wrap;
}

/* Merits & Disciplines (R12): the sheet's own parts, as compact as
   the Character drawer draws its sheet ([MV1]): one column of
   advantage groups, less padding. */
.seat-traits {
  gap: var(--ui-space-2);
}

.drawer[data-slot="left-bottom"] .sheet-block {
  padding: var(--ui-space-2);
}

.drawer[data-slot="left-bottom"] .sheet-advantage-groups {
  grid-template-columns: minmax(0, 1fr);
  gap: var(--ui-space-2);
}

.drawer[data-slot="left-bottom"] .sheet-trait {
  min-height: 1.5rem;
}

/* [/MV3] ---------------------------------------------------------- */

/* [MV5 — Missions & notes and Saved locations (map-view drawers MV5)] */

/* Four tabs now hang under the top group (R14): past the window's left
   edge they wrap, right-aligned, rather than run off it. */
.drawer[data-slot="top"] .drawer-tabs {
  flex-wrap: wrap;
  justify-content: flex-end;
  width: max-content;
  max-width: calc(100vw - var(--top-right) - var(--ui-space-2));
}

/* Missions & notes (R15): Open, Done and Notes under their own small
   headings, a done mission struck through on both pages. */
.top-drawer.records .records-section + .records-section {
  margin-top: var(--ui-space-3);
}

.top-drawer.records h3 {
  margin: 0;
  font-size: var(--ui-text-small);
  color: var(--ui-ink-muted);
  text-transform: uppercase;
  letter-spacing: 0.04em;
}

.top-drawer.records .missions-done .name,
.top-drawer.records .mission-done .note-head input[type="text"] {
  text-decoration: line-through;
  color: var(--ui-ink-muted);
}

/* The GM's: New mission and New note side by side, each note a card
   with its done tick over the shared fields. */
.top-drawer.records .records-new {
  display: flex;
  flex-wrap: wrap;
  gap: var(--ui-space-1);
  margin-bottom: var(--ui-space-2);
}

.top-drawer.records .records-new .top-door {
  margin: 0;
}

.top-drawer.records .note-card {
  padding: var(--ui-space-2) var(--ui-space-3);
}

.top-drawer.records .note-head .field.name {
  min-width: 8rem;
}

.top-drawer.records .mission-tick {
  display: inline-flex;
  align-items: center;
  gap: var(--ui-space-1);
  font-size: var(--ui-text-small);
}

/* Saved locations (R16): a place's shops are cards inside its own,
   under its Show on map. */
.top-drawer.places .player-list.shops {
  margin-top: var(--ui-space-2);
}

.top-drawer.places .player-list.shops li {
  padding: var(--ui-space-1) var(--ui-space-2);
}

/* [/MV5] ---------------------------------------------------------- */

/* [RC0 — the right-click menu (map context menu RC0, context_menu.rs)] ---
   One menu at the pointer, on the menus' layer: a card in sections,
   the top one untitled, each one after it under a line and its title.
   Rows are full-width buttons; a submenu hangs beside its row. Placed
   by the component (left/top), kept on screen there, never scrolled.
   --------------------------------------------------------------- */
.context-menu {
  position: fixed;
  z-index: var(--ui-z-menu);
  min-width: 12rem;
  max-width: calc(100vw - 2 * var(--ui-space-2));
  padding: var(--ui-space-1) 0;
  background: var(--ui-surface);
  color: var(--ui-ink);
  border: 1px solid var(--ui-edge);
  border-radius: var(--ui-radius-lg);
  box-shadow: var(--ui-elevation-2);
  font-size: var(--ui-text-small);
}

.context-menu:focus {
  outline: none;
}

.context-menu-section + .context-menu-section {
  margin-top: var(--ui-space-1);
  padding-top: var(--ui-space-1);
  border-top: 1px solid var(--ui-edge);
}

.context-menu-title {
  display: flex;
  align-items: baseline;
  gap: var(--ui-space-2);
  padding: var(--ui-space-1) var(--ui-space-3);
  color: var(--ui-ink-muted);
}

.context-menu-title .tag {
  font-size: var(--ui-text-caption);
  text-transform: uppercase;
  letter-spacing: 0.04em;
}

.context-menu-title .name {
  color: var(--ui-ink);
  font-weight: 600;
}

.context-menu-row {
  position: relative;
}

.context-menu-row > button {
  display: flex;
  align-items: center;
  gap: var(--ui-space-2);
  width: 100%;
  min-height: 2rem;
  padding: var(--ui-space-1) var(--ui-space-3);
  text-align: left;
  background: none;
  border: none;
  border-radius: 0;
}

.context-menu-row.current > button,
.context-menu-row > button:focus-visible {
  background: var(--ui-accent);
  outline: none;
}

.context-menu-row .flag {
  margin-left: auto;
  color: var(--ui-ink-muted);
  font-size: var(--ui-text-caption);
}

.context-menu-row .more {
  margin-left: auto;
  color: var(--ui-ink-muted);
}

.context-submenu {
  position: absolute;
  left: 100%;
  min-width: 10rem;
  padding: var(--ui-space-1) 0;
  background: var(--ui-surface);
  border: 1px solid var(--ui-edge);
  border-radius: var(--ui-radius-lg);
  box-shadow: var(--ui-elevation-2);
}

.context-submenu.flipped {
  left: auto;
  right: 100%;
}

/* A pin or a token the open menu is for: ringed in the selection's
   colour, dashed, so it never reads as the selection (map_ui/handle.rs's
   `set_menu_highlight`). */
[data-menu-for] {
  outline: 2px dashed var(--ui-select);
  outline-offset: 3px;
}

/* [/RC0] ---------------------------------------------------------- */

/* [MV7 — the GM's left, right and bottom edges (map-view drawers MV7)] */

/* A pane's head (gm_edges.rs): who the focus is to the table, and the
   door to its card. The slots themselves are the seat's (MV1, MV2):
   these key off what the GM's panes hold, or off a GM scope, never off
   the slot alone. */
.gm-edge-head {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--ui-space-2);
  margin-bottom: var(--ui-space-3);
}

.gm-edge-who {
  color: var(--ui-ink-muted);
  font-size: var(--ui-text-small);
}

/* One view, part A: the GM's left edge is the player's, with the GM's
   fields added by role. An item's GM fields are the card's own row
   (`.item-row`), a list row of their own under the player's row, which
   drags; the card's tools and the Money view's operations sit under
   what the player's drawer draws. */
.loadout-items > .item-row,
.loadout-slot-things > .item-row {
  margin: var(--ui-space-1) 0 var(--ui-space-2);
  font-size: var(--ui-text-small);
}

.loadout-card-gm,
.seat-money-gm {
  margin-top: var(--ui-space-3);
  padding-top: var(--ui-space-2);
  border-top: 1px dashed var(--ui-edge);
}

.gm-player-notes {
  margin-top: var(--ui-space-4);
}

.gm-player-note-list {
  list-style: none;
  margin: 0;
  padding: 0;
}

.gm-player-note + .gm-player-note {
  margin-top: var(--ui-space-3);
}

.gm-player-note-body {
  margin: var(--ui-space-1) 0 0;
  white-space: pre-wrap;
}

/* The GM's right group (D7) stands left of the dock while it is open,
   where a seat's covers its panel: the frame's clip then cuts the slide
   at the dock's edge, and it slides in from there. */
.drawer[data-scope="right-gm"] {
  right: var(--ui-chrome-right);
}

/* A Car or Nearby card: a thing, what kind it is and where it sits,
   then its items. */
.gm-holding + .gm-holding {
  margin-top: var(--ui-space-3);
}

.gm-holding h3 {
  margin: 0;
  font-size: var(--ui-text-body);
}

/* The bottom group (D8): Focus and the map tools as two tabs riding
   the panel's top edge, side by side and centred, as the slot's single
   tab is ([MV0]). Only the GM's page has a group in the bottom slot. */
.drawer[data-slot="bottom"] .drawer-tabs {
  bottom: 100%;
  left: 50%;
  translate: -50% 0;
  flex-direction: row;
}

.drawer[data-slot="bottom"] .drawer-tabs .drawer-tab {
  white-space: nowrap;
  border-bottom: 0;
  border-radius: var(--ui-radius-md) var(--ui-radius-md) 0 0;
}

.drawer[data-slot="bottom"] .drawer-tabs .drawer-tab[aria-expanded="true"] {
  border-color: var(--ui-accent);
}

/* The Focus chooser: a row of chips, wrapping on a narrow window. */
.drawer[data-scope="focus-gm"] .drawer-fan,
.drawer[data-scope="map-tools"] .drawer-fan:has(.gm-focus) {
  max-width: calc(100vw - var(--ui-chrome-right) - 2 * var(--ui-space-3));
}

.gm-focus {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: var(--ui-space-2);
}

.gm-chip {
  display: inline-flex;
  flex-direction: column;
  align-items: flex-start;
  padding: var(--ui-space-1) var(--ui-space-3);
  border: 1px solid var(--ui-edge);
  border-radius: var(--ui-radius-md);
  background: var(--ui-surface);
}

.gm-chip small {
  color: var(--ui-ink-muted);
  font-size: var(--ui-text-caption);
}

.gm-chip.npc {
  border-style: dashed;
}

.gm-chip[aria-pressed="true"] {
  background: var(--ui-surface-raised);
  box-shadow: inset 0 -2px 0 var(--ui-accent);
}

/* MapLibre's attribution, which discharges the ODbL credit, and its
   supplement rise over the Focus drawer alone as they do over the map
   tools' (MV6). */
:root:has(.drawer[data-scope="focus-gm"]) .maplibregl-ctrl-bottom-right {
  bottom: var(--drawer-bottom-reach, 0px);
}

:root:has(.drawer[data-scope="focus-gm"]) .map-attribution {
  bottom: calc(var(--ui-space-2) + var(--drawer-bottom-reach, 0px));
}

/* [/MV7] ---------------------------------------------------------- */

/* [MV8 — the frame's extras (map-view drawers MV8)] ----------------- */

/* Crowded edges (E14): a group's row on the top or the right shows its
   tabs' text while it fits, then their icons, then icons with the last
   tabs folded into a More tab, as drawers.rs measures it (data-crowd).
   Both a tab's text and its icon are always laid out, the one not shown
   taken out of the flow and hidden, so the row is measured whatever it
   shows; a folded tab is hidden the same way and never leaves the page,
   since MV9 numbers an edge's tabs from them (its V3). */
.drawer-tab .tab-icon {
  position: absolute;
  visibility: hidden;
  pointer-events: none;
}

.drawer-tabs:is([data-crowd="icons"], [data-crowd="more"]) .drawer-tab .tab-text {
  position: absolute;
  visibility: hidden;
  pointer-events: none;
}

.drawer-tabs:is([data-crowd="icons"], [data-crowd="more"]) .drawer-tab .tab-icon {
  position: static;
  visibility: inherit;
}

.drawer-tabs .drawer-tab[data-folded] {
  position: absolute;
  visibility: hidden;
  pointer-events: none;
}

/* An icon stands upright in the right's vertical tabs. */
.tab-icon {
  text-orientation: upright;
  font-weight: 600;
}

/* The More tab: drawn as a tab on its edge, lit while a drawer folded
   into it is out; its list opens beside it. */
.drawer-more {
  pointer-events: auto;
  white-space: nowrap;
  background: var(--ui-surface);
  border: 1px solid var(--ui-edge);
  font-family: var(--ui-font-data);
  font-variant-numeric: tabular-nums;
  color: var(--ui-ink-muted);
  box-shadow: var(--ui-elevation-1);
}

.drawer-more[data-holds-open] {
  border-color: var(--ui-accent);
  color: var(--ui-ink);
}

.drawer[data-slot="top"] .drawer-more {
  border-top: 0;
  border-radius: 0 0 var(--ui-radius-md) var(--ui-radius-md);
}

.drawer[data-slot="right"] .drawer-more {
  writing-mode: vertical-rl;
  text-orientation: upright;
  padding: var(--ui-space-3) var(--ui-space-1);
  border-right: 0;
  border-radius: var(--ui-radius-md) 0 0 var(--ui-radius-md);
}

.drawer-more-list {
  position: absolute;
  display: flex;
  flex-direction: column;
  gap: var(--ui-space-1);
  padding: var(--ui-space-2);
  min-width: 10rem;
  pointer-events: auto;
  background: var(--ui-surface);
  border: 1px solid var(--ui-edge);
  border-radius: var(--ui-radius-md);
  box-shadow: var(--ui-elevation-2);
}

.drawer-more-list[hidden] {
  display: none;
}

.drawer-more-list button {
  text-align: left;
}

.drawer-more-list button[aria-pressed="true"] {
  border-color: var(--ui-accent);
}

.drawer[data-slot="top"] .drawer-more-list {
  top: 100%;
  right: 0;
}

.drawer[data-slot="right"] .drawer-more-list {
  top: 0;
  right: 100%;
}

/* See-through (E12): an out drawer's panel shows the map through it at
   --drawer-glass (1 is solid), about half by default, which the GM's
   View menu slider sets on the root (drawers.rs leaves it alone, so a
   seat sees the default); under the pointer or with focus inside it
   turns near-solid, and a reader asking for less transparency gets it
   solid. The blur is on a box of the fan's own drawn behind the panel,
   never on the panel or anything around it: a filter there would make
   it the containing block of every fixed box inside it, and on an
   ancestor of .map-stage would shrink the map (the containing-block
   invariant above .map-stage). */
:root {
  --drawer-glass: 0.5;
  --drawer-glass-blur: 3px;
}

@media (prefers-reduced-transparency: reduce) {
  :root {
    --drawer-glass: 1 !important;
    --drawer-glass-blur: 0px;
  }
}

.drawer-fan .fan-panel {
  background: color-mix(in srgb, var(--ui-surface) calc(var(--drawer-glass) * 100%), transparent);
  text-shadow: 0 1px 2px rgb(0 0 0 / calc((1 - var(--drawer-glass)) * 0.8));
  transition:
    transform var(--ui-duration-base) var(--ui-ease-standard),
    background-color var(--ui-duration-fast) var(--ui-ease-standard);
}

.drawer-fan:is(:hover, :focus-within) .fan-panel {
  background: color-mix(in srgb, var(--ui-surface) calc(max(var(--drawer-glass), 0.88) * 100%), transparent);
  text-shadow: none;
}

.drawer-fan {
  position: relative;
  z-index: 0;
}

.drawer-fan::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  backdrop-filter: blur(var(--drawer-glass-blur));
}

/* The slider in the GM's View menu. */
.drawer-glass input[type="range"] {
  width: 100%;
}

/* A drawer popped out as a window (E13) keeps its tab on its edge,
   dashed while it floats, out or in. */
.drawer-tab[data-floats] {
  border-style: dashed;
}

/* A drawer popped out as a window (E13, drawers.rs's DrawerWindow): a
   box of its own beside the slot's frame, placed by left and top under
   the bar, never by a transform, sized by its corner and dragged by its
   head. While the window is in, the box stays in the page empty and
   unseen at the bar's foot, which a pop-out reads. It paints over the
   docked panels and their tabs, the one in front highest. */
.drawer-window {
  position: fixed;
  display: flex;
  box-sizing: border-box;
  pointer-events: auto;
  min-width: 14rem;
  min-height: 6rem;
  max-width: calc(100vw - 1rem);
  max-height: calc(100dvh - var(--ui-chrome-top) - var(--save-banner-h) - 1rem);
  overflow: hidden;
  resize: both;
  border: 1px solid var(--ui-edge);
  border-radius: var(--ui-radius-lg);
  box-shadow: 0 10px 30px rgb(0 0 0 / 0.55);
}

.drawer-window[data-afloat="false"] {
  visibility: hidden;
  pointer-events: none;
  width: 0;
  height: 0;
  min-width: 0;
  min-height: 0;
  border: 0;
  resize: none;
}

.drawer-window.collapsed {
  min-height: 0;
  resize: none;
}

.drawer-window:focus-within {
  border-color: var(--ui-accent);
}

.drawer-window-fan {
  position: relative;
  z-index: 0;
  display: flex;
  flex: 1;
  min-width: 0;
  min-height: 0;
}

.drawer-window-fan::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  backdrop-filter: blur(var(--drawer-glass-blur));
}

.drawer-window .fan-panel {
  background: color-mix(in srgb, var(--ui-surface) calc(var(--drawer-glass) * 100%), transparent);
  text-shadow: 0 1px 2px rgb(0 0 0 / calc((1 - var(--drawer-glass)) * 0.8));
}

.drawer-window:is(:hover, :focus-within) .fan-panel {
  background: color-mix(in srgb, var(--ui-surface) calc(max(var(--drawer-glass), 0.88) * 100%), transparent);
  text-shadow: none;
}

.drawer-window .fan-head {
  cursor: grab;
  user-select: none;
  touch-action: none;
}

.drawer-window.dragging .fan-head {
  cursor: grabbing;
}

.drawer-window.collapsed .fan-panel > :not(.fan-head) {
  display: none;
}

.drawer-window .fan-head button,
.drawer-fan .drawer-pop-out {
  font-size: var(--ui-text-small);
}

/* [/MV8] ---------------------------------------------------------- */

/* [K4 — the palette's specifiers (command palette K4, wave D)] ------ */

/* What narrows the palette to a group, at its heading's far end: the
   letter and a drawn space (letters are the one way since wave E's
   E1). The heading is upper case and what is typed is not, so the
   key keeps its case. */
.command-specifier {
  float: right;
  padding: 0 var(--ui-space-1);
  border: 1px solid var(--ui-edge);
  border-radius: var(--ui-radius-sm);
  color: var(--ui-ink-muted);
  font-family: var(--ui-font-data);
  font-size: var(--ui-text-caption);
  letter-spacing: 0;
  text-transform: none;
}

/* [/K4] ----------------------------------------------------------- */

/* [wave F — keys (F1, vim_keys.rs and vim_motion.rs)] ---------------- */

/* The tab holding the keys (vim refinements item 4): ringed, with no
   digit drawn on it; the digits are in the edge's guide, and still jump. */
.drawer-tab[data-vim-here] {
  outline: 2px solid var(--ui-select);
  outline-offset: -2px;
}

/* The row the keys moved to (F1.5): marked rows take focus without
   being buttons, so they get the ring a button has. */
[data-vim-row]:focus-visible,
[data-vim-col][data-vim-scroll]:focus-visible {
  outline: 2px solid var(--ui-select);
  outline-offset: -2px;
}

/* A field the keys stand beside (vim refinements item 6, the grid): its
   row holds the keys and the field is lit as their cell, until Enter
   starts the typing. */
.vim-at {
  outline: 2px solid var(--ui-select);
  outline-offset: 1px;
}

/* A drawer's search prompt (F1.6), over its foot; vim_motion.rs places
   it from the drawer's box and puts it on the body. */
.vim-search {
  position: fixed;
  z-index: var(--ui-z-menu);
  display: flex;
  align-items: center;
  gap: var(--ui-space-2);
  box-sizing: border-box;
  height: 2.5rem;
  padding: 0 var(--ui-space-2);
  background: var(--ui-surface-raised);
  border-top: 1px solid var(--ui-select);
  box-shadow: var(--ui-elevation-2);
  font-family: var(--ui-font-data);
}

.vim-search-slash {
  color: var(--ui-select);
  font-weight: 700;
}

.vim-search input {
  flex: 1;
  min-width: 0;
}

.vim-search-note {
  color: var(--ui-warning);
  font-size: var(--ui-text-caption);
  white-space: nowrap;
}

/* [/wave F — keys] ------------------------------------------------- */

/* [wave F — table and menu (F2, table_ui.rs and home_menu.rs)] ------- */

/* The Table drawer (F2.4): a card per player. The online green is mixed
   from the accent and the ink, so it follows the theme like the rest. */
.table-drawer,
.home-menu {
  --table-online: color-mix(in srgb, var(--ui-accent) 45%, var(--ui-ink));
}
.table-drawer {
  display: grid;
  gap: var(--ui-space-3);
}
.table-sum {
  margin: 0;
  color: var(--ui-ink-muted);
  font-size: var(--ui-text-caption);
}
.table-seats {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: var(--ui-space-2);
}
.table-seat {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  gap: var(--ui-space-1) var(--ui-space-3);
  align-items: center;
  padding: var(--ui-space-2) var(--ui-space-3);
  background: var(--ui-surface);
  border: 1px solid var(--ui-edge);
  border-radius: var(--ui-radius-md);
}
.table-seat.off {
  opacity: 0.6;
}
/* The card a palette pick of its player lands on (G6). */
.table-seat:focus-visible {
  outline: 2px solid var(--ui-accent);
  outline-offset: 2px;
}
.table-dot {
  width: 0.7rem;
  height: 0.7rem;
  box-sizing: border-box;
  border-radius: 50%;
  border: 2px solid var(--ui-edge);
}
.table-dot.here {
  background: var(--table-online);
  border-color: var(--table-online);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--table-online) 20%, transparent);
}
.table-who {
  min-width: 0;
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--ui-space-2);
}
.table-who .name {
  font: normal var(--ui-text-body) / 1.2 var(--ui-font-prose);
  color: var(--ui-ink);
  background: none;
  border: 0;
  padding: 0;
  cursor: text;
}
.table-who input.name {
  min-width: 8rem;
  border-bottom: 1px solid var(--ui-accent);
}
.table-who .state {
  font-size: var(--ui-text-caption);
  color: var(--ui-ink-muted);
}
.table-who .state.here {
  color: var(--table-online);
}
.table-actions {
  display: flex;
  align-items: center;
  gap: var(--ui-space-1);
}
.table-actions > .meta {
  font-size: var(--ui-text-caption);
  color: var(--ui-ink-muted);
}
.table-btn {
  display: inline-flex;
  align-items: center;
  gap: var(--ui-space-1);
  white-space: nowrap;
  cursor: pointer;
  font-size: var(--ui-text-caption);
  color: var(--ui-ink);
  background: var(--ui-surface-raised);
  border: 1px solid var(--ui-edge);
  border-radius: var(--ui-radius-md);
  padding: var(--ui-space-1) var(--ui-space-2);
}
.table-btn:hover {
  border-color: var(--ui-ink-muted);
}
.table-btn.primary {
  background: var(--ui-accent);
  border-color: var(--ui-accent);
}
.table-btn.danger {
  border-color: var(--ui-blood);
  color: var(--ui-error);
}
.table-btn.icon {
  width: 1.7rem;
  height: 1.7rem;
  padding: 0;
  justify-content: center;
}
.table-more {
  position: relative;
}
.table-pop {
  position: absolute;
  right: 0;
  top: calc(100% + var(--ui-space-1));
  z-index: var(--ui-z-menu);
  min-width: 15rem;
  display: grid;
  gap: 2px;
  padding: var(--ui-space-1);
  background: var(--ui-surface-raised);
  border: 1px solid var(--ui-edge);
  border-radius: var(--ui-radius-lg);
  box-shadow: var(--ui-elevation-2);
}
.table-pop > button {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: var(--ui-space-3);
  width: 100%;
  text-align: left;
  cursor: pointer;
  color: var(--ui-ink);
  background: none;
  border: 0;
  border-radius: var(--ui-radius-md);
  padding: var(--ui-space-1) var(--ui-space-2);
  font-size: var(--ui-text-small);
}
.table-pop > button:hover,
.table-pop > button:focus-visible {
  background: color-mix(in srgb, var(--ui-accent) 40%, transparent);
}
.table-pop > button.danger {
  color: var(--ui-error);
}
.table-pop .meta {
  font-size: var(--ui-text-caption);
  color: var(--ui-ink-muted);
}
.table-pop .meta.silent {
  display: none;
}
.table-pop > p.meta {
  margin: 0;
  padding: 0 var(--ui-space-2);
}
.table-pop .sep {
  height: 1px;
  margin: var(--ui-space-1);
  background: var(--ui-edge);
}
.table-pop .pool {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: var(--ui-space-3);
  padding: var(--ui-space-1) var(--ui-space-2);
  font-size: var(--ui-text-small);
}
.table-pop .pool input {
  width: 3.5rem;
}
.table-links {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: var(--ui-space-1);
}
.table-link {
  display: flex;
  align-items: end;
  gap: var(--ui-space-1);
  padding: 0 var(--ui-space-2);
}
.table-link .field {
  flex: 1;
  min-width: 0;
}
.table-link input {
  width: 100%;
  font-size: var(--ui-text-caption);
}
.table-chars {
  grid-column: 2 / -1;
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--ui-space-1);
}
.table-chars > .meta {
  font-size: var(--ui-text-caption);
  color: var(--ui-ink-muted);
}
.table-chip {
  display: inline-flex;
  align-items: center;
  border: 1px solid var(--ui-edge);
  border-radius: 999px;
  font-size: var(--ui-text-caption);
}
.table-chip.playing {
  border-color: var(--ui-accent);
  background: color-mix(in srgb, var(--ui-accent) 28%, transparent);
}
.table-chip button {
  color: var(--ui-ink);
  background: none;
  border: 0;
  cursor: pointer;
  font-size: inherit;
}
.table-chip .play {
  display: inline-flex;
  align-items: center;
  gap: var(--ui-space-1);
  padding: 2px var(--ui-space-1) 2px 2px;
}
.table-chip .av {
  width: 1.3rem;
  height: 1.3rem;
  display: grid;
  place-items: center;
  border-radius: 50%;
  font: 0.7rem var(--ui-font-prose);
  background: var(--ui-surface-raised);
  border: 1px solid var(--ui-edge);
}
.table-chip.playing .av {
  border-color: var(--ui-accent);
}
.table-chip .drop {
  color: var(--ui-ink-muted);
  padding: 0 var(--ui-space-2) 0 0;
}
.table-chip .drop:hover {
  color: var(--ui-ink);
}
.table-chip-add {
  cursor: pointer;
  color: var(--ui-ink-muted);
  background: none;
  border: 1px dashed var(--ui-edge);
  border-radius: 999px;
  padding: 2px var(--ui-space-2);
  font-size: var(--ui-text-caption);
}
.table-shown {
  grid-column: 2 / -1;
  font-size: var(--ui-text-caption);
}
.table-link-button {
  cursor: pointer;
  color: var(--ui-ink-muted);
  background: none;
  border: 0;
  padding: 0;
  font-size: inherit;
}
.table-link-button:hover {
  color: var(--ui-ink);
  text-decoration: underline;
}
.table-confirm {
  grid-column: 1 / -1;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--ui-space-2);
  padding-top: var(--ui-space-1);
  border-top: 1px solid var(--ui-edge);
  font-size: var(--ui-text-caption);
}
.table-seat > .status {
  grid-column: 2 / -1;
  margin: 0;
  font-size: var(--ui-text-caption);
  color: var(--ui-ink-muted);
}
.table-seat > .status:empty {
  display: none;
}
.table-add {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--ui-space-2);
}
.table-add input {
  flex: 1;
  min-width: 10rem;
}
@media (max-width: 560px) {
  .table-seat {
    grid-template-columns: auto minmax(0, 1fr);
  }
  .table-actions {
    grid-column: 1 / -1;
    justify-content: flex-end;
  }
}

/* The main menu (F2.6): a dialog over a dimmed map, at the dialog
   layer with the palette. */
.home-scrim {
  position: fixed;
  inset: 0;
  z-index: var(--ui-z-dialog);
  background: var(--ui-scrim);
  backdrop-filter: blur(2px);
}
.home-menu {
  position: fixed;
  left: 50%;
  top: 50%;
  z-index: var(--ui-z-dialog);
  transform: translate(-50%, -50%);
  width: min(26rem, calc(100vw - 2rem));
  box-sizing: border-box;
  display: grid;
  gap: var(--ui-space-4);
  padding: var(--ui-space-5) var(--ui-space-5) var(--ui-space-4);
  color: var(--ui-ink);
  background: var(--ui-surface-raised);
  border: 1px solid var(--ui-edge);
  border-radius: var(--ui-radius-lg);
  box-shadow: var(--ui-elevation-2);
}
.home-head {
  display: grid;
  gap: var(--ui-space-1);
}
.home-head .eyebrow {
  font-size: var(--ui-text-caption);
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--ui-ink-muted);
}
.home-head h1 {
  margin: 0;
  font: normal 1.6rem / 1.15 var(--ui-font-prose);
}
.home-head .night {
  color: var(--ui-ink-muted);
  font-size: var(--ui-text-small);
  font-variant-numeric: tabular-nums;
}
.home-choices {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: var(--ui-space-1);
}
.home-choice {
  width: 100%;
  display: grid;
  grid-template-columns: 1.6rem 2rem minmax(0, 1fr) auto;
  gap: var(--ui-space-3);
  align-items: center;
  padding: var(--ui-space-2) var(--ui-space-3);
  text-align: left;
  cursor: pointer;
  color: var(--ui-ink);
  background: var(--ui-surface);
  border: 1px solid var(--ui-edge);
  border-radius: var(--ui-radius-md);
}
.home-choice.current,
.home-choice:hover {
  border-color: var(--ui-accent);
  background: color-mix(in srgb, var(--ui-accent) 22%, var(--ui-surface));
}
.home-choice svg {
  width: 1.3rem;
  height: 1.3rem;
  justify-self: center;
  color: var(--ui-ink-muted);
}
.home-choice.current svg {
  color: var(--ui-ink);
}
.home-choice .what b {
  display: block;
  font: normal var(--ui-text-body) / 1.2 var(--ui-font-prose);
}
.home-choice .what small {
  display: block;
  font-size: var(--ui-text-caption);
  color: var(--ui-ink-muted);
}
.home-choice .aside {
  font-size: var(--ui-text-caption);
  color: var(--ui-ink-muted);
  white-space: nowrap;
}
.home-menu kbd {
  justify-self: center;
  padding: 0 var(--ui-space-1);
  font: 600 0.7rem / 1.3 var(--ui-font-data);
  color: var(--ui-ink);
  background: var(--ui-surface);
  border: 1px solid var(--ui-edge);
  border-bottom-width: 2px;
  border-radius: var(--ui-radius-sm);
}
.home-choice.current kbd {
  border-color: var(--ui-accent);
}
.home-foot {
  display: flex;
  flex-wrap: wrap;
  gap: var(--ui-space-1) var(--ui-space-4);
  font-size: var(--ui-text-caption);
  color: var(--ui-ink-muted);
}

/* The bar's Menu button, at its left. */
.topbar .menu-button {
  display: inline-flex;
  align-items: center;
  gap: var(--ui-space-1);
  border-color: var(--ui-accent);
}
.topbar .menu-button svg {
  width: 0.9rem;
  height: 0.9rem;
}

/* [/wave F — table and menu] --------------------------------------- */

/* [wave F — prep (F3, the rail, prep_entities.rs and the books)] ----- */

/* Prep (wave F, F3.1): the rail of eight books beside the open one, the
   whole overlay one screen high. The rail and each book scroll on their
   own, so a long book never carries the rail away, and every column
   S3's motion moves over is its own scroller. Scoped by `.prep`, which
   outranks the overlay's older flex rules above. */
.data-overlay.prep.open {
  display: grid;
  grid-template-columns: 12.5rem minmax(0, 1fr);
  align-items: stretch;
  gap: 0;
  padding: 0;
  overflow: hidden;
}

.prep .data-rail {
  position: static;
  flex: none;
  min-height: 0;
  overflow-y: auto;
  padding: var(--ui-space-4) var(--ui-space-2);
  border-right: 1px solid var(--ui-edge);
}

.prep .rail-title {
  margin: 0 0 var(--ui-space-2) var(--ui-space-2);
  font: normal var(--ui-text-title) / 1.2 var(--ui-font-prose);
}

.prep .rail-group {
  margin: var(--ui-space-3) 0 var(--ui-space-1) var(--ui-space-2);
  font: 0.68rem var(--ui-font-data);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--ui-ink-muted);
}

.prep .data-rail ul {
  gap: 2px;
}

.prep .rail-item {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto 2.1rem;
  gap: var(--ui-space-2);
  align-items: center;
  padding: var(--ui-space-1) var(--ui-space-2);
  border: 1px solid transparent;
  font-family: var(--ui-font-data);
  font-size: var(--ui-text-small);
}

.prep .rail-item .label {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.prep .rail-item .n {
  font-size: 0.7rem;
  color: var(--ui-ink-muted);
  font-variant-numeric: tabular-nums;
}

.prep .rail-item .key,
.prep .rail-keys kbd {
  justify-self: end;
  padding: 0 0.3rem;
  border: 1px solid var(--ui-edge);
  border-bottom-width: 2px;
  border-radius: var(--ui-radius-md);
  background: var(--ui-surface);
  color: var(--ui-ink-muted);
  font: 600 0.66rem / 1.2 var(--ui-font-data);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

.prep .rail-item.current {
  background: color-mix(in srgb, var(--ui-accent) 30%, transparent);
  border-color: var(--ui-accent);
}

.prep .rail-item.current .key {
  border-color: var(--ui-ink-muted);
  color: var(--ui-ink);
}

.prep .rail-note {
  min-height: 1.2em;
  margin: var(--ui-space-2) var(--ui-space-2) 0;
  font-size: var(--ui-text-caption);
  color: var(--ui-warning);
}

.prep .rail-note:empty {
  min-height: 0;
  margin: 0;
}

.prep .rail-keys {
  display: grid;
  gap: var(--ui-space-1);
  margin-top: var(--ui-space-4);
  padding: var(--ui-space-2);
  border-top: 1px solid var(--ui-edge);
  font-size: 0.7rem;
  color: var(--ui-ink-muted);
}

/* The open book fills the space right of the rail and scrolls itself;
   a one-page book keeps a reading column, a split one the full width. */
.prep .panels {
  max-width: none;
  margin: 0;
  min-height: 0;
  height: 100%;
  overflow: hidden;
}

.prep .panel.active {
  height: 100%;
  box-sizing: border-box;
}

.prep .panel.single.active {
  overflow-y: auto;
  padding: var(--ui-space-4) var(--ui-space-6) var(--ui-space-12);
}

.prep .panel.single > * {
  max-width: 52rem;
}

.prep .panel.single:focus {
  outline: none;
  box-shadow: inset 2px 0 0 var(--ui-select);
}

.prep .panel-title {
  font: normal var(--ui-text-display) / 1.15 var(--ui-font-prose);
}

/* A split book's title is its list's to say (prep_entities). */
.prep .panel.split > .panel-title {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* Data's two views, as a pair of buttons. */
.data-views {
  display: flex;
  gap: var(--ui-space-1);
  margin-block: var(--ui-space-3);
}

.data-views [aria-pressed="true"] {
  border-color: var(--ui-accent);
  background: var(--ui-accent);
  color: var(--ui-ink);
}

/* The one-page books in the mockup's face (F3.2): the title with what
   is in the book beside it, and each section a raised card, two to a
   row where the book pairs them. Every section keeps its own controls;
   only their frame changes. */
.prep .page-title {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--ui-space-1) var(--ui-space-3);
  margin-bottom: var(--ui-space-4);
}

.prep .page-title .panel-title {
  margin: 0;
}

.prep .page-sub {
  color: var(--ui-ink-muted);
  font-size: var(--ui-text-small);
}

.prep .prep-cols {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(17rem, 1fr));
  gap: var(--ui-space-4);
  align-items: start;
  margin-bottom: var(--ui-space-4);
}

.prep .panel.single section {
  box-sizing: border-box;
  min-width: 0;
}

.prep .panel.single > section,
.prep .panel.single > .prep-cols > section,
.prep .panel.single > div > section.resolved {
  margin: 0 0 var(--ui-space-4);
  padding: var(--ui-space-3) var(--ui-space-4);
  background: var(--ui-surface-raised);
  border-radius: var(--ui-radius-lg);
}

.prep .prep-cols > section {
  margin: 0;
}

.prep .panel.single section > h2 {
  margin: 0 0 var(--ui-space-2);
  font: normal var(--ui-text-body) / 1.2 var(--ui-font-prose);
}

/* Notes and Library are one section each: the book's title is theirs,
   so the section's own heading stands down. */
.prep #panel-notes .notes-section > .section-head h2,
.prep #panel-library .library > h2 {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

.prep #panel-data .notices {
  margin-top: var(--ui-space-6);
  font-size: var(--ui-text-caption);
  color: var(--ui-ink-muted);
}

/* Characters and Things (F3.3): the list on the left, the record of
   its selected row on the right, each scrolling on its own. */
.prep .panel.split.active {
  overflow: hidden;
}

.prep-split {
  height: 100%;
  display: grid;
  grid-template-columns: 19rem minmax(0, 1fr);
}

.prep-list {
  min-width: 0;
  min-height: 0;
  display: grid;
  /* One column no wider than the 19rem track: without it the column takes
     its widest child's width and spills into the record beside it. */
  grid-template-columns: minmax(0, 1fr);
  grid-template-rows: auto minmax(0, 1fr) auto;
  border-right: 1px solid var(--ui-edge);
  background: color-mix(in srgb, var(--ui-surface) 80%, black);
}

.prep-list .list-head {
  display: grid;
  gap: var(--ui-space-2);
  padding: var(--ui-space-4) var(--ui-space-3) var(--ui-space-2);
  border-bottom: 1px solid var(--ui-edge);
}

.prep-list .list-title {
  display: flex;
  align-items: baseline;
  gap: var(--ui-space-2);
  margin: 0;
  font: normal var(--ui-text-title) / 1.2 var(--ui-font-prose);
}

.prep-list .list-title small {
  font: var(--ui-text-caption) var(--ui-font-data);
  color: var(--ui-ink-muted);
}

.prep-list .search input {
  width: 100%;
  box-sizing: border-box;
}

.prep-list .filters {
  display: flex;
  flex-wrap: wrap;
  gap: var(--ui-space-1);
}

.prep-list .filters button {
  padding: 0.1rem var(--ui-space-2);
  border-radius: 999px;
  background: none;
  font-size: 0.72rem;
  color: var(--ui-ink-muted);
}

.prep-list .filters [aria-pressed="true"] {
  border-color: var(--ui-ink-muted);
  background: color-mix(in srgb, var(--ui-accent) 25%, transparent);
  color: var(--ui-ink);
}

.prep-list .rows {
  overflow-y: auto;
  padding: var(--ui-space-1) var(--ui-space-1) var(--ui-space-3);
}

.prep-list .grp {
  display: flex;
  justify-content: space-between;
  margin: var(--ui-space-3) var(--ui-space-2) var(--ui-space-1);
  font: 0.68rem var(--ui-font-data);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--ui-ink-muted);
}

.prep-row {
  display: grid;
  grid-template-columns: 2rem minmax(0, 1fr) auto;
  gap: var(--ui-space-2);
  align-items: center;
  width: 100%;
  padding: var(--ui-space-1) var(--ui-space-2);
  text-align: left;
  background: none;
  border: 1px solid transparent;
  border-radius: var(--ui-radius-md);
  font-family: var(--ui-font-data);
}

.prep-row:hover {
  background: var(--ui-surface-raised);
}

.prep-row[aria-selected="true"] {
  background: var(--ui-surface-raised);
  border-color: var(--ui-accent);
}

.prep-row:focus-visible {
  outline-offset: -2px;
}

.prep-row .av {
  display: grid;
  place-items: center;
  width: 2rem;
  height: 2rem;
  border: 1px solid var(--ui-edge);
  border-radius: 50%;
  background: var(--ui-surface-raised);
  font: 0.95rem var(--ui-font-prose);
}

.prep-row .who {
  min-width: 0;
}

.prep-row b,
.prep-row small {
  display: block;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.prep-row b {
  font-weight: 500;
}

.prep-row small,
.prep-row .tag {
  font-size: 0.72rem;
  color: var(--ui-ink-muted);
}

.prep-row .tag {
  white-space: nowrap;
}

.prep-row .tag.play {
  color: var(--ui-accent);
  filter: brightness(1.8);
}

.prep-list .empty-list {
  padding: var(--ui-space-4) var(--ui-space-2);
  font-size: var(--ui-text-caption);
  color: var(--ui-ink-muted);
}

.prep-list .list-foot {
  display: flex;
  gap: var(--ui-space-1);
  padding: var(--ui-space-2) var(--ui-space-3);
  border-top: 1px solid var(--ui-edge);
}

.prep-list .list-foot input {
  flex: 1;
  min-width: 0;
}

.prep-record {
  min-width: 0;
  min-height: 0;
  overflow-y: auto;
  padding: var(--ui-space-4) var(--ui-space-6) var(--ui-space-12);
}

.prep-record:focus {
  outline: none;
  box-shadow: inset 2px 0 0 var(--ui-select);
}

.prep-record > * {
  max-width: 46rem;
}

.prep-record .nothing {
  padding: var(--ui-space-12) var(--ui-space-4);
  text-align: center;
  color: var(--ui-ink-muted);
}

/* The record (F3.4): its head, the facts under it, then one section a
   block, each headed by what it is. The blocks inside keep their own
   styles from the card they came from. */
.entity-record {
  display: grid;
  gap: var(--ui-space-4);
}

.entity-record .rec-head {
  display: grid;
  grid-template-columns: 3.4rem minmax(0, 1fr) auto;
  gap: var(--ui-space-3);
  align-items: center;
}

.entity-record .av {
  display: grid;
  place-items: center;
  width: 3.4rem;
  height: 3.4rem;
  border: 1px solid var(--ui-edge);
  border-radius: var(--ui-radius-lg);
  background: var(--ui-surface-raised);
  font: 1.5rem var(--ui-font-prose);
}

.entity-record.cast .av {
  border-radius: 50%;
}

.entity-record .rec-name {
  display: grid;
  gap: var(--ui-space-1);
  min-width: 0;
}

.entity-record .rec-title {
  width: 100%;
  padding: 0;
  background: none;
  border: 0;
  border-bottom: 1px solid transparent;
  border-radius: 0;
  font: normal var(--ui-text-display) / 1.15 var(--ui-font-prose);
}

.entity-record .rec-title:hover {
  border-bottom-color: var(--ui-edge);
}

.entity-record .rec-title:focus {
  outline: none;
  border-bottom-color: var(--ui-accent);
}

.entity-record .line {
  display: flex;
  flex-wrap: wrap;
  gap: var(--ui-space-1) var(--ui-space-3);
  align-items: center;
  font-size: var(--ui-text-caption);
  color: var(--ui-ink-muted);
}

.entity-record .pill {
  padding: 0.05rem var(--ui-space-2);
  border: 1px solid var(--ui-edge);
  border-radius: 999px;
  background: none;
  font-size: 0.72rem;
  color: var(--ui-ink);
}

.entity-record .pill.play {
  border-color: var(--ui-accent);
}

.entity-record .facts {
  display: grid;
  gap: var(--ui-space-1);
  padding: var(--ui-space-2) var(--ui-space-3);
  background: var(--ui-surface-raised);
  border-radius: var(--ui-radius-md);
  font-size: var(--ui-text-small);
}

.entity-record .facts > * {
  margin: 0;
}

.entity-record .rec-sec {
  display: grid;
  gap: var(--ui-space-2);
  padding-top: var(--ui-space-3);
  border-top: 1px solid var(--ui-edge);
}

.entity-record .rec-sec:empty {
  display: none;
}

.entity-record .sec-head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--ui-space-2);
}

.entity-record .sec-head h3 {
  margin: 0;
  font: normal var(--ui-text-body) / 1.2 var(--ui-font-prose);
}

.entity-record .sec-head .hint {
  font-size: var(--ui-text-caption);
  color: var(--ui-ink-muted);
}

.entity-record .rec-more {
  margin: 0;
}

.entity-record .rec-more .quiet {
  background: none;
  font-size: var(--ui-text-caption);
  color: var(--ui-ink-muted);
}

@media (max-width: 600px) {
  .prep-split {
    grid-template-columns: minmax(0, 1fr);
    grid-template-rows: minmax(0, 40%) minmax(0, 1fr);
  }

  .prep-list {
    border-right: 0;
    border-bottom: 1px solid var(--ui-edge);
  }
}

@media (max-width: 860px) {
  .data-overlay.prep.open {
    grid-template-columns: 3.6rem minmax(0, 1fr);
  }

  .prep .rail-title,
  .prep .rail-group,
  .prep .rail-item .label,
  .prep .rail-item .n,
  .prep .rail-keys {
    display: none;
  }

  .prep .rail-item {
    grid-template-columns: 1fr;
    justify-items: center;
  }
}

/* [/wave F — prep] ------------------------------------------------- */

/* [Drawer polish (docs/manifests/2026-10-07-drawer-polish.md)] -------- */

/* No tab hangs past its open panel (items 1 and 2, the mockup's rule):
   a side panel is at least as tall as its edge's column of tabs, and a
   top panel at least as wide as its row, never past the window's left
   edge. The sizes are the row's own, which drawers.rs's TabExtent
   writes on the slot box. */
.drawer:is([data-slot^="left"], [data-slot="right"]) .drawer-slide > .drawer-fan {
  min-height: var(--drawer-tabs-h, 0px);
}

.drawer[data-slot="top"] .drawer-slide > .drawer-fan {
  min-width: min(var(--drawer-tabs-w, 0px), calc(100vw - var(--top-right) - var(--ui-space-2)));
}

/* The right's tabs stand beside a seat's Seen rail (or the GM's open
   dock), never on it (item 4): while in, the box that slides sits left
   by the rail's width, so the tabs stand at its edge, where an open
   panel, which covers the rail, puts them too. While a Messages or
   Notes fan is open the slot itself has stepped left of it ([MV2]), so
   the box stays put. */
.drawer[data-slot="right"] {
  --right-tabs-at: var(--ui-chrome-right);
}

.player-shell:has(.seat-talk) .drawer[data-slot="right"] {
  --right-tabs-at: 0px;
}

.drawer[data-slot="right"] .drawer-slide:not(:has(> .drawer-fan)) {
  transform: translateX(calc(100% - var(--right-tabs-at)));
}

/* MapLibre's zoom and compass step left of the right's tabs (item 3),
   one tab column past the rail. An open right panel covers the rail on
   a seat's page, beside them; on the GM's it drops below them
   (gm_edges.rs's RIGHT_CLEAR_OF). The Clock, which keeps 3rem clear of
   the controls, steps with them, as does everything placed by its
   inset. */
:root {
  --ui-right-tabs: 2rem;
}

:root:has(.drawer[data-slot="right"]) .maplibregl-ctrl-top-right {
  right: calc(var(--ui-chrome-right) + var(--ui-right-tabs));
}

@media (min-width: 641px) {
  :root:has(.drawer[data-slot="right"]) :is(.drawer[data-slot="top-clock"], .drawer[data-slot="top"], .map-status-stack, .player-status) {
    --clock-right: calc(var(--ui-chrome-right) + 3rem + var(--ui-right-tabs));
  }
}

/* The tabs as the mockup draws them (item 5): see-through a little less
   than the panels at the same --drawer-glass, the map blurred behind
   them, in muted ink that lifts on hover; the open one in its panel's
   colour with the accent edge. The blur is a box of the tab's own drawn
   behind it, as the panel's is ([MV8]); `isolation` keeps that box
   inside the tab without moving the tab's own stacking. A slot's rule
   that places its tab (the Clock's and the bottom's hang absolute)
   outranks the `position` here. */
.drawer-tab,
.drawer-more {
  position: relative;
  isolation: isolate;
  background: color-mix(in srgb, var(--ui-surface) calc((0.25 + var(--drawer-glass) * 0.7) * 100%), transparent);
  color: var(--ui-ink-muted);
  font-size: var(--ui-text-caption);
  letter-spacing: 0.04em;
  text-shadow: 0 1px 2px rgb(0 0 0 / 0.85);
  transition:
    color var(--ui-duration-fast) var(--ui-ease-standard),
    background-color var(--ui-duration-fast) var(--ui-ease-standard),
    border-color var(--ui-duration-fast) var(--ui-ease-standard);
}

.drawer-tab::before,
.drawer-more::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  border-radius: inherit;
  pointer-events: none;
  backdrop-filter: blur(var(--drawer-glass-blur));
}

.drawer-tab:hover,
.drawer-more:hover {
  color: var(--ui-ink);
}

.drawer[data-slot] .drawer-slide .drawer-tab[aria-expanded="true"],
.drawer[data-slot] .drawer-slide .drawer-more[data-holds-open] {
  background: color-mix(in srgb, var(--ui-surface) calc(var(--drawer-glass) * 100%), transparent);
  color: var(--ui-ink);
  border-color: var(--ui-accent);
}

/* Pop out is the mockup's small icon button in the panel's head
   (item 6). */
.drawer-fan .drawer-pop-out {
  display: inline-grid;
  place-items: center;
  width: 1.8rem;
  height: 1.8rem;
  padding: 0;
  color: var(--ui-ink-muted);
}

.drawer-fan .drawer-pop-out:hover {
  color: var(--ui-ink);
}

.drawer-pop-out svg {
  width: 0.95rem;
  height: 0.95rem;
  display: block;
}

/* The Character drawer fits without a scroll bar (item 7, the user:
   "far to tall … I don't want to see an ugly scroll bar"). A closed left
   group no longer takes height under an open panel: its tabs step out
   beside it by --stack-beside, which drawers.rs's LeftStack writes, so
   a panel alone has the whole column. The sheet widens to lay Physical,
   Social and Mental side by side and the trackers four a row, folding
   back as the window narrows. Where it still overflows (a short
   window), the bar is there but unseen until the pointer is over it,
   so nothing shifts as it shows. */
.drawer[data-slot^="left"] .drawer-tabs {
  translate: var(--stack-beside, 0px) 0;
  transition: translate var(--ui-duration-base) var(--ui-ease-standard);
}

.drawer[data-slot="left"] .drawer-fan:has(.sheet-columns) {
  width: min(44rem, calc(100vw - 3rem));
}

.drawer[data-slot="left"] .sheet-columns {
  grid-template-columns: repeat(auto-fit, minmax(min(12rem, 100%), 1fr));
}

.drawer[data-slot="left"] .sheet-tracks {
  grid-template-columns: repeat(auto-fit, minmax(min(9rem, 100%), 1fr));
}

.drawer[data-slot="left"] .sheet-trait {
  min-height: 1.35rem;
}

/* Under the sheet, the tray's and the play's status lines take a line
   each while they say nothing; empty, they keep their place for a
   screen reader (role="status") but draw nothing, as .visually-hidden
   does. */
.drawer[data-slot="left"] .seat-characters-column .status:empty {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

.drawer[data-slot="left"] .seat-characters-column .seat-declined:empty {
  display: none;
}

.drawer[data-slot^="left"] .fan-panel {
  scrollbar-width: thin;
  scrollbar-color: transparent transparent;
}

.drawer[data-slot^="left"] .fan-panel:hover {
  scrollbar-color: var(--ui-edge) transparent;
}

/* The skill check (items 8–12, skill_check.rs), as the mockup's .check
   draws it: the pool line on top, the Attribute grid to start from, then
   the group buttons with their options beside them in as many columns
   as fit, so a full sheet's skills show whole. Every rule is under
   .check, whose class names are short and common. */
.drawer[data-slot="left"] .drawer-fan:has(.check) {
  width: min(40rem, calc(100vw - 3rem));
}

.check {
  display: flex;
  flex-direction: column;
  gap: var(--ui-space-2);
  font-size: var(--ui-text-small);
  container-type: inline-size;
}

.check .muted {
  color: var(--ui-ink-muted);
}

.check .check-step {
  margin: var(--ui-space-1) 0 0;
  font-size: var(--ui-text-caption);
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ui-ink-muted);
}

.check .pool-line {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--ui-space-1);
  padding: var(--ui-space-2);
  border: 1px solid var(--ui-edge);
  border-radius: var(--ui-radius-md);
  font-size: var(--ui-text-caption);
}

.check .part {
  padding: 0.1rem 0.45rem;
  font-size: var(--ui-text-caption);
}

.check .pool-words {
  margin-left: auto;
  color: var(--ui-ink-muted);
}

.check .pick-grid {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--ui-space-2);
}

.check :is(.pick-col, .pick-list, .cat-list) {
  display: grid;
  gap: var(--ui-space-1);
  align-content: start;
}

.check .unusual {
  grid-template-columns: repeat(3, minmax(0, 1fr));
}

.check .sub {
  margin: 0;
  font-size: 0.66rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ui-ink-muted);
}

.check .pick {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: var(--ui-space-2);
  width: 100%;
  min-height: 2rem;
  padding: 0.25rem 0.5rem;
  text-align: left;
  font-size: var(--ui-text-caption);
}

.check .pick.zero > span:first-child {
  color: var(--ui-ink-muted);
}

.check .pick.held {
  border-color: var(--ui-accent);
  background: color-mix(in srgb, var(--ui-accent) 30%, transparent);
}

.check .spec {
  display: block;
  font-size: 0.66rem;
  font-style: italic;
  color: var(--ui-ink-muted);
}

.check .more-btn {
  text-align: left;
  border-style: dashed;
  color: var(--ui-ink-muted);
  font-size: var(--ui-text-caption);
}

.check .cats {
  display: grid;
  grid-template-columns: 9rem minmax(0, 1fr);
  gap: var(--ui-space-2);
  align-items: start;
}

.check .cat {
  display: flex;
  align-items: center;
  justify-content: space-between;
  min-height: 2.4rem;
  text-align: left;
}

.check .cat[aria-expanded="true"] {
  border-color: var(--ui-accent);
  background: color-mix(in srgb, var(--ui-accent) 30%, transparent);
}

.check .fly {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(8.5rem, 1fr));
  gap: var(--ui-space-1);
  align-content: start;
  padding: var(--ui-space-1);
  border: 1px solid var(--ui-edge);
  border-radius: var(--ui-radius-md);
}

.check .fly > p {
  grid-column: 1 / -1;
  margin: var(--ui-space-1);
}

.check .full-head {
  display: flex;
  align-items: center;
  gap: var(--ui-space-2);
}

.check .btn-row {
  display: flex;
  flex-wrap: wrap;
  gap: var(--ui-space-2);
}

.check .btn {
  display: inline-flex;
  align-items: center;
  gap: var(--ui-space-1);
}

.check .btn.primary {
  background: var(--ui-accent);
  border-color: var(--ui-accent);
  color: var(--ui-ink);
}

.check kbd {
  font: 600 0.66rem/1.2 var(--ui-font-data);
  padding: 0 0.3rem;
  border: 1px solid var(--ui-edge);
  border-bottom-width: 2px;
  border-radius: 3px;
  background: var(--ui-surface);
  color: var(--ui-ink);
  text-shadow: none;
}

.check :is(.preroll, .result) {
  padding: var(--ui-space-2);
  border: 1px solid var(--ui-edge);
  border-radius: var(--ui-radius-md);
}

.check .preroll {
  display: grid;
  gap: var(--ui-space-2);
  border-color: var(--ui-accent);
}

.check .opt {
  display: grid;
  grid-template-columns: auto auto 1fr;
  gap: var(--ui-space-2);
  align-items: center;
  font-size: var(--ui-text-caption);
}

.check .result p {
  margin: var(--ui-space-1) 0 0;
}

.check .check-heading {
  font-weight: 600;
}

.check .dice {
  display: flex;
  flex-wrap: wrap;
  gap: 0.25rem;
  margin-top: var(--ui-space-1);
}

.check .die {
  width: 1.6rem;
  height: 1.6rem;
  display: grid;
  place-items: center;
  border: 1px solid var(--ui-edge);
  border-radius: 4px;
  background: var(--ui-surface);
  color: var(--ui-ink-muted);
  font-size: var(--ui-text-caption);
  font-variant-numeric: tabular-nums;
}

.check .die.hit {
  color: var(--ui-ink);
  border-color: var(--ui-ink-muted);
}

.check .die.ten {
  color: var(--ui-select);
  border-color: var(--ui-select);
}

.check .die.hunger {
  background: color-mix(in srgb, var(--ui-blood) 30%, var(--ui-surface));
  border-color: var(--ui-blood);
}

.check .die.hunger.one {
  box-shadow: 0 0 0 2px var(--ui-blood);
}

.check .roll-total {
  font: normal var(--ui-text-lead) var(--ui-font-prose);
}

.check .flag {
  color: var(--ui-warning);
  font-size: var(--ui-text-caption);
}

.check .check-keys {
  margin: 0;
  font-size: var(--ui-text-caption);
}

/* Narrow, the columns fold to one. */
@container (max-width: 26rem) {
  .check :is(.pick-grid, .unusual, .cats) {
    grid-template-columns: minmax(0, 1fr);
  }
}

/* In use, the check stands out (item 12, the user: "more pronounced"):
   under the pointer or holding the keyboard its panel, already near
   solid there, takes a bright edge. */
.drawer-fan:has(.check):is(:hover, :focus-within) .fan-panel {
  border-color: var(--ui-select);
  box-shadow:
    0 0 0 1px var(--ui-select),
    0 14px 36px rgb(0 0 0 / 0.65);
}

/* [/Drawer polish] ------------------------------------------------- */

/* [Keys and scale (docs/manifests/2026-10-07-keys-and-scale.md)] ----- */

/* The key status line (item 3, key_line.rs): bottom left, beside the
   left tab column, on both pages, above what the bottom drawers reach
   (a seat's tray, the GM's Focus and map tools), and on the GM's page
   above the map's attribution too, which holds the corner. It takes no
   click but its command line's (item 7). */
.key-line {
  position: fixed;
  z-index: calc(var(--ui-z-menu) + 1);
  left: calc(2rem + var(--ui-space-2));
  bottom: calc(var(--drawer-bottom-reach, 0px) + var(--ui-space-2));
  display: flex;
  align-items: stretch;
  max-width: calc(100vw - 2rem - 2 * var(--ui-space-2));
  font: var(--ui-text-caption)/1 var(--ui-font-data);
  border: 1px solid var(--ui-edge);
  border-radius: var(--ui-radius-md);
  background: color-mix(in srgb, var(--ui-surface) 88%, transparent);
  box-shadow: var(--ui-elevation-2);
  pointer-events: none;
}

.shell .key-line {
  bottom: calc(max(var(--drawer-bottom-reach, 0px), 3.4rem) + var(--ui-space-2));
}

/* The room the line takes above the bottom drawers' reach, in pixels
   (drawers.rs's LeftStack reads it), which the left panels stop short
   of; the GM's line stands higher, over the attribution. */
:root:has(.key-line) {
  --key-line-room: 44px;
}

:root:has(.shell .key-line) {
  --key-line-room: 72px;
}

.drawer[data-slot="left"] .drawer-fan {
  max-height: min(var(--stack-max, 100dvh), calc(100dvh - var(--ui-chrome-top) - var(--save-banner-h) - var(--drawer-drop, 0px) - var(--drawer-bottom-reach, 0px) - var(--key-line-room, 0px) - var(--ui-space-4)));
}

.key-line > * {
  display: flex;
  align-items: center;
  padding: 0.35rem 0.55rem;
  white-space: nowrap;
}

.key-mode {
  font-weight: 700;
  letter-spacing: 0.08em;
  background: var(--ui-edge);
  color: var(--ui-ink);
  border-radius: calc(var(--ui-radius-md) - 1px) 0 0 calc(var(--ui-radius-md) - 1px);
}

.key-line:is([data-mode="space"], [data-mode="edge"]) .key-mode {
  background: var(--ui-select);
  color: var(--ui-surface);
}

.key-line[data-mode="drawer"] .key-mode {
  background: var(--ui-accent);
}

.key-line[data-mode="typing"] .key-mode {
  background: color-mix(in srgb, var(--ui-accent) 40%, #3b5b7a);
}

.key-line:is([data-mode="search"], [data-mode="find"]) .key-mode {
  background: var(--ui-warning);
  color: var(--ui-surface);
}

.key-line[data-mode="command"] .key-mode {
  background: var(--ui-ink);
  color: var(--ui-surface);
}

.key-where {
  color: var(--ui-ink);
  border-right: 1px solid var(--ui-edge);
}

.key-where.empty {
  display: none;
}

.key-hint {
  gap: 0.2rem;
  color: var(--ui-ink-muted);
  overflow: hidden;
  text-overflow: ellipsis;
}

.key-line kbd {
  font: 700 0.68rem/1 var(--ui-font-data);
  padding: 0.1rem 0.3rem;
  border: 1px solid var(--ui-edge);
  border-bottom-width: 2px;
  border-radius: 3px;
  background: var(--ui-surface-raised);
  color: var(--ui-ink);
}

.key-no {
  color: var(--ui-warning);
}

/* The guide (item 4): above the line, with the leader armed, on an
   edge's tabs, or pinned by `?`. */
.key-guide {
  position: fixed;
  z-index: calc(var(--ui-z-menu) + 1);
  left: calc(2rem + var(--ui-space-2));
  bottom: calc(var(--drawer-bottom-reach, 0px) + var(--ui-space-2) + 2.4rem);
  box-sizing: border-box;
  width: min(36rem, calc(100vw - 2rem - 2 * var(--ui-space-2)));
  max-height: calc(100dvh - var(--ui-chrome-top) - 6rem);
  overflow: auto;
  padding: var(--ui-space-3);
  font-size: var(--ui-text-caption);
  background: color-mix(in srgb, var(--ui-surface) 95%, transparent);
  border: 1px solid var(--ui-select);
  border-radius: var(--ui-radius-md);
  box-shadow: var(--ui-elevation-2);
  pointer-events: none;
}

.shell .key-guide {
  bottom: calc(max(var(--drawer-bottom-reach, 0px), 3.4rem) + var(--ui-space-2) + 2.4rem);
}

.key-guide h3 {
  margin: 0 0 var(--ui-space-2);
  font: 600 var(--ui-text-caption) var(--ui-font-data);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--ui-ink-muted);
}

.key-guide-edges {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(8rem, 1fr));
  gap: var(--ui-space-2) var(--ui-space-3);
}

.key-guide-edge b {
  display: flex;
  gap: 0.3rem;
  align-items: center;
  margin-bottom: 0.25rem;
}

.key-guide-rows {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: 0.15rem;
  color: var(--ui-ink-muted);
}

.key-guide-rows li {
  display: flex;
  gap: 0.4rem;
}

.key-guide-rows li.here {
  color: var(--ui-ink);
}

.key-digit {
  min-width: 1.1rem;
  color: var(--ui-select);
  font-variant-numeric: tabular-nums;
}

.key-guide-foot {
  margin: var(--ui-space-2) 0 0;
  padding-top: var(--ui-space-2);
  border-top: 1px solid var(--ui-edge);
  color: var(--ui-ink-muted);
}

.key-guide kbd {
  font: 700 0.68rem/1 var(--ui-font-data);
  padding: 0.1rem 0.3rem;
  border: 1px solid var(--ui-edge);
  border-bottom-width: 2px;
  border-radius: 3px;
  background: var(--ui-surface-raised);
  color: var(--ui-ink);
}

/* A drawer's search narrows as you type (item 5, vim_motion.rs): the
   first match lit, the rows that do not match faded. */
.vim-hit {
  outline: 1px solid var(--ui-select);
  outline-offset: 1px;
}

.vim-faded {
  opacity: 0.35;
}

/* The command line (item 7): inside the status line, taking its clicks
   and its keys; the usage of what is typed beside it. */
.key-command {
  gap: var(--ui-space-2);
  pointer-events: auto;
}

.key-command input {
  width: min(16rem, 40vw);
  padding: 0.15rem 0.3rem;
  font: var(--ui-text-small) var(--ui-font-data);
  color: var(--ui-ink);
  background: var(--ui-surface);
  border: 1px solid var(--ui-edge);
  border-radius: var(--ui-radius-md);
}

.key-usage {
  color: var(--ui-ink-muted);
  overflow: hidden;
  text-overflow: ellipsis;
}

/* A roll's card, above the line (item 7). */
.key-card {
  position: fixed;
  z-index: calc(var(--ui-z-menu) + 1);
  left: calc(2rem + var(--ui-space-2));
  bottom: calc(var(--drawer-bottom-reach, 0px) + var(--ui-space-2) + 2.4rem);
  box-sizing: border-box;
  width: min(24rem, calc(100vw - 2rem - 2 * var(--ui-space-2)));
  padding: var(--ui-space-2) var(--ui-space-3);
  font-size: var(--ui-text-small);
  background: color-mix(in srgb, var(--ui-surface) 96%, transparent);
  border: 1px solid var(--ui-edge);
  border-radius: var(--ui-radius-md);
  box-shadow: var(--ui-elevation-2);
}

.shell .key-card {
  bottom: calc(max(var(--drawer-bottom-reach, 0px), 3.4rem) + var(--ui-space-2) + 2.4rem);
}

.key-card p {
  margin: 0;
}

.key-card-title {
  font-weight: 600;
}

.key-card-dice {
  display: flex;
  flex-wrap: wrap;
  gap: 0.25rem;
  margin: var(--ui-space-2) 0;
}

.key-die {
  width: 1.6rem;
  height: 1.6rem;
  display: grid;
  place-items: center;
  border: 1px solid var(--ui-edge);
  border-radius: 4px;
  background: var(--ui-surface);
  color: var(--ui-ink-muted);
  font-variant-numeric: tabular-nums;
}

.key-die.hit {
  color: var(--ui-ink);
  border-color: var(--ui-ink-muted);
}

.key-die.ten {
  color: var(--ui-select);
  border-color: var(--ui-select);
}

.key-die.hunger {
  background: color-mix(in srgb, var(--ui-blood) 30%, var(--ui-surface));
  border-color: var(--ui-blood);
}

.key-card-total {
  font: var(--ui-text-lead) var(--ui-font-prose);
}

.key-card-flag {
  color: var(--ui-warning);
  font-size: var(--ui-text-caption);
}

.key-card-note {
  margin-top: var(--ui-space-1) !important;
  color: var(--ui-ink-muted);
  font-size: var(--ui-text-caption);
}

/* The address search's panel (item 11), above the line. */
.key-places {
  position: fixed;
  z-index: calc(var(--ui-z-menu) + 1);
  left: calc(2rem + var(--ui-space-2));
  bottom: calc(var(--drawer-bottom-reach, 0px) + var(--ui-space-2) + 2.4rem);
  box-sizing: border-box;
  width: min(26rem, calc(100vw - 2rem - 2 * var(--ui-space-2)));
  max-height: 50dvh;
  overflow: auto;
  padding: var(--ui-space-2);
  background: color-mix(in srgb, var(--ui-surface) 96%, transparent);
  border: 1px solid var(--ui-warning);
  border-radius: var(--ui-radius-md);
  box-shadow: var(--ui-elevation-2);
}

.shell .key-places {
  bottom: calc(max(var(--drawer-bottom-reach, 0px), 3.4rem) + var(--ui-space-2) + 2.4rem);
}

.key-places-head {
  margin: 0 0 var(--ui-space-1);
  font-size: var(--ui-text-caption);
  color: var(--ui-ink-muted);
}

.key-places-rows {
  display: grid;
  gap: 0.2rem;
}

.key-place {
  display: flex;
  justify-content: space-between;
  gap: var(--ui-space-2);
  text-align: left;
}

.key-place-meta {
  color: var(--ui-ink-muted);
  font-size: var(--ui-text-caption);
  white-space: nowrap;
}

/* The address under the pointer, heading the map's right-click menu
   (item 12). */
.context-note {
  margin: 0 0 var(--ui-space-1);
  padding: var(--ui-space-1) var(--ui-space-2) var(--ui-space-2);
  border-bottom: 1px solid var(--ui-edge);
  font-size: var(--ui-text-caption);
  color: var(--ui-ink-muted);
}

/* [/Keys and scale] ------------------------------------------------ */

/* [work-order: places — the spot menu, building pins and pings (PLACES)] */

/* A building's pin (map_ui/buildings.rs): a marker's house pin, with no
   selection and no drag behind a click, so the pointer says its menu is
   the way in. */
.gm-building {
  cursor: context-menu;
}

/* A locked building's pin wears a lock at its shoulder (P7), in the
   warning ink on the raised surface, so it reads at a glance and never as
   the menu's or the selection's mark. */
.gm-building .glyph {
  position: relative;
}

.gm-building .lock {
  position: absolute;
  top: -0.25rem;
  right: -0.375rem;
  width: 0.875rem;
  height: 0.875rem;
  padding: 0.125rem;
  border-radius: 50%;
  background: var(--ui-surface-raised);
  color: var(--ui-warning);
  box-shadow: 0 0 0 1px currentColor;
}

/* A ping (map_ui/ping.rs): a ring that pulses on the spot and the name of
   who sent it under it, fading out over the 6 s `ping::SHOWN_MS` keeps
   it. A "look here" rings in the accent, so the table tells a pull of
   its view from a point. Never in the pointer's way. */
.map-ping {
  pointer-events: none;
  width: 2.5rem;
  height: 2.5rem;
  color: var(--ui-select);
  animation: map-ping-fade 6s linear forwards;
}

.map-ping.look {
  color: var(--ui-accent);
}

.map-ping .ring {
  display: block;
  width: 100%;
  height: 100%;
  border-radius: 50%;
  border: 3px solid currentColor;
  animation: map-ping-pulse 1.2s var(--ui-ease-standard) infinite;
}

.map-ping .label {
  position: absolute;
  top: 100%;
  left: 50%;
  transform: translateX(-50%);
  margin-top: 0.125rem;
  font-family: var(--ui-font-data);
  font-size: var(--ui-text-caption);
  line-height: 1.3;
  white-space: nowrap;
  background: color-mix(in srgb, var(--ui-surface) 85%, transparent);
  color: var(--ui-ink);
  padding: 0 var(--ui-space-1);
  border-radius: var(--ui-radius-sm);
  box-shadow: 0 0 0 1px currentColor;
}

@keyframes map-ping-fade {
  0%,
  70% {
    opacity: 1;
  }
  100% {
    opacity: 0;
  }
}

@keyframes map-ping-pulse {
  from {
    transform: scale(0.4);
    opacity: 1;
  }
  to {
    transform: scale(1.3);
    opacity: 0.35;
  }
}

@media (prefers-reduced-motion: reduce) {
  .map-ping .ring {
    animation: none;
  }
}

/* [/work-order: places] -------------------------------------------- */

/* [work-order: pieces — boxes, discs, lock glyphs and badges (PIECES)] */
/* The occupants' box (K2, map_ui/boxes.rs): a frame over a car or a
   building's pin, and in it the discs of those riding or inside, each a
   token's disc at a token's size. The frame is only a backdrop sized by
   the code; the discs are markers of their own over it and take the
   pointer. Both sit above the street's pieces. */
.gm-box {
  box-sizing: border-box;
  pointer-events: none;
  z-index: 1;
  border-radius: var(--ui-radius-lg);
  background: color-mix(in srgb, var(--ui-surface) 85%, transparent);
  border: 1px solid var(--ui-linked);
  box-shadow: 0 2px 6px rgba(0, 0, 0, var(--ui-shadow-alpha-2));
}

.gm-token.gm-box-disc {
  z-index: 2;
}

:root[data-theme="night"] .gm-box {
  border-color: var(--ui-accent);
}
/* "Send <seat> inside?" (K8, map_ui/occupants.rs' keep_following): the
   GM's map asks before it moves a player's view, over the building's
   box, and nothing moves until Send. */
.gm-follow {
  z-index: 3;
  display: flex;
  align-items: center;
  gap: var(--ui-space-1);
  padding: var(--ui-space-1) var(--ui-space-2);
  border-radius: var(--ui-radius-lg);
  background: var(--ui-surface);
  border: 1px solid var(--ui-select);
  box-shadow: 0 2px 6px rgba(0, 0, 0, var(--ui-shadow-alpha-2));
  font-size: var(--ui-text-small);
  white-space: nowrap;
}

.gm-follow button {
  font-size: inherit;
}

/* A locked car's padlock (K6, map_ui/tokens.rs' style_token): a small
   body and its shackle at the disc's top right, in the selection's ink
   so it reads on any token colour. GM-only, as players do not see locks
   before they try. */
.gm-token .lock {
  position: absolute;
  top: -0.125rem;
  right: -0.3125rem;
  width: 0.5625rem;
  height: 0.4375rem;
  border-radius: 1px;
  background: var(--ui-select);
  box-shadow: 0 0 0 1px var(--ui-surface);
  pointer-events: none;
}

.gm-token .lock::before {
  content: "";
  position: absolute;
  left: 50%;
  bottom: 100%;
  transform: translateX(-50%);
  box-sizing: border-box;
  width: 0.375rem;
  height: 0.3125rem;
  border: 1.5px solid var(--ui-select);
  border-bottom: none;
  border-radius: 0.25rem 0.25rem 0 0;
}

/* A token's or a disc's status badges (K9, map_ui/tokens.rs'
   badge_spans): a row of small letters over the disc's top left, each
   named in full by its title. A status the GM set is solid; one the
   sheet says is outlined, since nobody set it. GM-only in this build.
   `.badge` alone is the letter, wherever a status is shown. */
.gm-token .badges {
  position: absolute;
  top: -0.375rem;
  left: -0.375rem;
  display: flex;
  gap: 1px;
  pointer-events: none;
}

.badge {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  box-sizing: border-box;
  min-width: 0.75rem;
  height: 0.75rem;
  padding: 0 1px;
  border-radius: var(--ui-radius-sm);
  background: var(--ui-warning);
  color: var(--ui-surface);
  box-shadow: 0 0 0 1px var(--ui-surface);
  font-family: var(--ui-font-data);
  font-size: 0.5625rem;
  font-weight: 700;
  line-height: 1;
}

.badge.from-sheet {
  background: var(--ui-surface);
  color: var(--ui-warning);
  box-shadow: inset 0 0 0 1px var(--ui-warning);
}
/* [/work-order: pieces] -------------------------------------------- */

/* [work-order: drawers — Characters, groups, the Table and scenes (DRAWERS)] */

/* The Characters drawer (C1): a row per person and NPC, its name a
   button that focuses them, the focused one edged in the accent. */
.characters-list .characters-pick {
  display: block;
  padding: 0;
  text-align: left;
  background: none;
  border: none;
  color: inherit;
  font: inherit;
  cursor: pointer;
}

.characters-list .characters-pick:hover .name {
  text-decoration: underline;
}

.characters-list li.focused {
  border-color: var(--ui-accent);
}

/* A row's Group picker (C2), beside its kind. */
.characters-list .characters-group {
  margin-top: var(--ui-space-1);
  font-size: var(--ui-text-small);
}

/* The groups as directories (C2): a heading per group, its name a
   button that renames it, Delete asking first. */
.characters-section + .characters-section {
  margin-top: var(--ui-space-3);
}

.characters-heading {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--ui-space-2);
  margin: 0;
}

.characters-heading .characters-group-name {
  padding: 0;
  background: none;
  border: none;
  color: inherit;
  font: inherit;
  cursor: pointer;
}

.characters-heading .top-door {
  margin: 0;
  font-size: var(--ui-text-small);
  font-weight: normal;
}

.characters-confirm {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--ui-space-1);
  font-size: var(--ui-text-small);
  font-weight: normal;
}

.characters-new-group {
  margin-top: var(--ui-space-3);
}

/* New NPC and New character (C3), above New group. */
.characters-new {
  margin-top: var(--ui-space-3);
}

.characters-new + .characters-new-group {
  margin-top: var(--ui-space-2);
}

/* Status badges in the drawers (C5): a small pill per badge, one read
   off the sheet dashed; scoped here, the map's `.badge` being PIECES'. */
.status-badges {
  display: inline-flex;
  flex-wrap: wrap;
  gap: var(--ui-space-1);
  margin-left: var(--ui-space-1);
  vertical-align: middle;
}

.status-badges .badge {
  padding: 0 var(--ui-space-1);
  border: 1px solid var(--ui-accent);
  border-radius: var(--ui-radius-md);
  font-size: var(--ui-text-small);
  font-weight: normal;
  line-height: 1.4;
}

.status-badges .badge.sheet {
  border-style: dashed;
  color: var(--ui-ink-muted);
}

/* A Characters row's Status control (C5): ticks under its summary. */
.characters-list .status-control {
  display: inline-block;
  margin: var(--ui-space-1) var(--ui-space-1) 0 0;
  font-size: var(--ui-text-small);
}

.characters-list .status-control summary {
  cursor: pointer;
}

.characters-list .status-control ul {
  list-style: none;
  margin: var(--ui-space-1) 0 0;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  gap: var(--ui-space-2);
}

.characters-list .status-control li {
  border: none;
  padding: 0;
}

/* The Table at a glance (C6): the player's initials on their colour,
   their state in a word, and who they play as on a line of its own. */
.table-who .table-face {
  display: inline-grid;
  place-items: center;
  width: 1.8rem;
  height: 1.8rem;
  align-self: center;
  border-radius: 50%;
  background: var(--face-fill);
  color: var(--face-ink);
  font-size: var(--ui-text-caption);
  font-weight: bold;
  letter-spacing: 0.02em;
}

.table-who .table-glance {
  padding: 0 var(--ui-space-1);
  border: 1px solid var(--ui-edge);
  border-radius: var(--ui-radius-md);
  font-size: var(--ui-text-caption);
  color: var(--ui-ink-muted);
}

.table-who .table-glance[data-glance="online"] {
  border-color: var(--table-online);
  color: var(--table-online);
}

.table-who .table-glance[data-glance="open"] {
  border-color: var(--ui-accent);
  color: var(--ui-ink);
}

.table-who .table-playing {
  flex-basis: 100%;
  font-size: var(--ui-text-caption);
  color: var(--ui-ink-muted);
}

/* Scenes at the Table's foot (C7): a row per scene, its name renaming
   it, Inside of, what it shows behind a summary, Delete asking first;
   the tick lists side by side and scrolled within, never the drawer. */
.scenes {
  display: grid;
  gap: var(--ui-space-2);
}

.scenes-heading {
  margin: 0;
}

.scene-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: var(--ui-space-2);
}

.scene-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--ui-space-1) var(--ui-space-3);
  padding: var(--ui-space-2) var(--ui-space-3);
  border: 1px solid var(--ui-edge);
  border-radius: var(--ui-radius-md);
}

.scene-head {
  display: flex;
  align-items: baseline;
  gap: var(--ui-space-2);
  flex: 1 1 12rem;
}

.scene-head .scene-name {
  padding: 0;
  background: none;
  border: none;
  color: inherit;
  font: inherit;
  font-weight: bold;
  cursor: pointer;
}

.scene-head .meta,
.scene-inside {
  font-size: var(--ui-text-caption);
  color: var(--ui-ink-muted);
}

.scene-inside select {
  margin-left: var(--ui-space-1);
}

.scene-edit {
  flex-basis: 100%;
  font-size: var(--ui-text-caption);
}

.scene-edit summary {
  cursor: pointer;
}

.scene-edit[open] {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(10rem, 1fr));
  gap: var(--ui-space-2);
}

.scene-edit[open] > summary {
  grid-column: 1 / -1;
}

.scene-ticks {
  margin: 0;
  padding: var(--ui-space-1) var(--ui-space-2);
  border: 1px solid var(--ui-edge);
  border-radius: var(--ui-radius-md);
}

.scene-ticks ul {
  list-style: none;
  margin: 0;
  padding: 0;
  max-height: 12rem;
  overflow-y: auto;
}

.scene-confirm {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--ui-space-1);
  font-size: var(--ui-text-caption);
}

.scene-new {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--ui-space-2);
}

.scene-new .status {
  flex-basis: 100%;
  margin: 0;
  font-size: var(--ui-text-caption);
  color: var(--ui-ink-muted);
}

.scene-new .status:empty {
  display: none;
}

/* A scene's Send seats (C9): a tick per switched-on player, Everyone
   first, then Send. */
.scene-send {
  flex-basis: 100%;
  font-size: var(--ui-text-caption);
}

.scene-send summary {
  cursor: pointer;
}

.scene-send ul {
  list-style: none;
  margin: var(--ui-space-1) 0;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  gap: var(--ui-space-1) var(--ui-space-3);
}

/* A card's scene (C8): where the seat was sent, and Send to scene,
   under its characters. */
.table-scene {
  grid-column: 2 / -1;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--ui-space-1) var(--ui-space-3);
  font-size: var(--ui-text-caption);
}

.table-scene .meta {
  color: var(--ui-ink-muted);
}

.table-send select {
  margin-left: var(--ui-space-1);
}

/* [/work-order: drawers] ------------------------------------------- */

/* [work-order: seat — what players see and do (SEAT)] -------------- */

/* A note on a thing (Q8): who reads it, and the button that lets go of
   the thing, on one line under the composer's heading. */
.note-on {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--ui-space-2);
  margin: 0;
  font-size: var(--ui-text-small);
  color: var(--ui-ink-muted);
}

/* [/work-order: seat] ---------------------------------------------- */

/* [work-order: folders — the Files panel (FOLDERS)] ---------------- */
/* An element's folder (folder_ui.rs): the panel on a card, a drawer or
   an item row, closed behind its Files button, and the window a menu
   opens over the map, under the menus. */
.folder-panel {
  display: flex;
  flex-direction: column;
  gap: var(--ui-space-1);
}
.folder-panel > .folder-toggle {
  align-self: flex-start;
}
.folder-body {
  display: flex;
  flex-direction: column;
  gap: var(--ui-space-2);
  font-family: var(--ui-font-data);
  font-size: var(--ui-text-small);
}
.folder-files {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--ui-space-1);
}
.folder-file {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: var(--ui-space-2);
}
.folder-file-name {
  flex: 1 1 10rem;
  overflow-wrap: anywhere;
}
.folder-page-file .folder-file-name {
  font-style: italic;
}
.folder-heading {
  margin: 0;
  font-size: var(--ui-text-small);
  color: var(--ui-ink-muted);
}
.folder-page,
.folder-empty,
.folder-told {
  margin: 0;
  color: var(--ui-ink-muted);
}
.folder-told:empty {
  display: none;
}
.folder-add {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--ui-space-2);
}
.folder-window {
  position: fixed;
  top: calc(var(--ui-chrome-top) + var(--save-banner-h) + var(--ui-space-4));
  right: calc(var(--ui-chrome-right) + var(--ui-space-4));
  z-index: var(--ui-z-tools);
  width: min(28rem, calc(100vw - 2 * var(--ui-space-4)));
  max-height: calc(100vh - var(--ui-chrome-top) - var(--save-banner-h) - 2 * var(--ui-space-4));
  overflow: auto;
  display: flex;
  flex-direction: column;
  gap: var(--ui-space-3);
  padding: var(--ui-space-3);
  background: var(--ui-surface-raised);
  color: var(--ui-ink);
  border: 1px solid var(--ui-edge);
  border-radius: var(--ui-radius-lg);
  box-shadow: var(--ui-elevation-2);
}
.folder-window-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--ui-space-2);
}
.folder-window-head h2 {
  margin: 0;
  font-size: var(--ui-text-lead);
}
/* [/work-order: folders] ------------------------------------------- */

/* [Night polish (the user, 2026-10-11: "dark mode is janky")] ------- */

/* MapLibre's own credit box is white glass with black ink, made for the
   day raster; on the night map it was a bright patch. In Night it takes
   the UI's surface and muted ink, as the supplement on the left does. */
:root[data-theme="night"] .maplibregl-ctrl.maplibregl-ctrl-attrib {
  background: color-mix(in srgb, var(--ui-surface) 85%, transparent);
  color: var(--ui-ink-muted);
}

:root[data-theme="night"] .maplibregl-ctrl-attrib a {
  color: var(--ui-ink-muted);
}

/* The night map's labels are bright on a dark ground, so at the default
   half glass they read through a panel (the Clock's street names); in
   Night a resting panel keeps at least 0.8 of its surface, whatever the
   View menu's slider says. Under the pointer or focus it is as before. */
:root[data-theme="night"] .drawer-fan:not(:hover, :focus-within) .fan-panel {
  background: color-mix(in srgb, var(--ui-surface) calc(max(var(--drawer-glass), 0.8) * 100%), transparent);
}

/* The dial's face is the panel's own colour, which on the night ground
   left only its hands: its rim takes the muted ink there. */
:root[data-theme="night"] .clock-rim {
  stroke: var(--ui-ink-muted);
}

/* [/Night polish] -------------------------------------------------- */

/* [one view, part B] The drawer switches: a fan from the bar's right
   corner, opened from View → Drawers… or Tools → Drawers… and ;drawers.
   It takes the menus' card treatment (.view-menu, .player-fan): the
   root is the frame and never moves, the .fan-panel paints. Rows by
   edge, the viewer's own column first, the GM's per-seat columns after;
   wide enough for every column on a wide screen, scrolling past that.
   Tokens only (docs/designs/drawer-switches-mockup.html). */
.drawer-switches-fan {
  position: fixed;
  top: var(--ui-chrome-top);
  right: var(--ui-space-3);
  max-width: calc(100vw - 2 * var(--ui-space-3));
  max-height: calc(100vh - var(--ui-chrome-top) - var(--ui-space-3));
  z-index: var(--ui-z-menu);
  display: flex;
}

.drawer-switches-fan .fan-panel {
  border: 1px solid var(--ui-edge);
  border-radius: var(--ui-radius-lg);
  box-shadow: var(--ui-elevation-2);
  overflow: auto;
}

.drawer-switches {
  display: grid;
  gap: var(--ui-space-2);
}

.switch-grid {
  border-collapse: collapse;
  font-size: var(--ui-text-small);
  font-variant-numeric: tabular-nums;
}

.switch-grid th,
.switch-grid td {
  text-align: left;
  padding: var(--ui-space-1) var(--ui-space-2);
  border-bottom: 1px solid var(--ui-edge);
  white-space: nowrap;
}

.switch-grid thead th {
  font: 600 var(--ui-text-caption) var(--ui-font-data);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ui-ink-muted);
  vertical-align: bottom;
}

.switch-grid .switch-col {
  text-align: center;
}

.switch-grid .switch-col small {
  display: block;
  font-weight: 400;
  letter-spacing: 0;
  text-transform: none;
}

.switch-grid .switch-col.dim {
  opacity: 0.55;
}

.switch-edge td {
  padding-top: var(--ui-space-3);
  font: 600 var(--ui-text-caption) var(--ui-font-data);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--ui-warning);
  border-bottom: 0;
}

.switch-digit {
  color: var(--ui-ink-muted);
  font-size: var(--ui-text-caption);
}

.switch-name {
  font-weight: 600;
}

.switch-row.here .switch-name {
  color: var(--ui-select);
}

.switch-cell {
  text-align: center;
}

.switch-toggle {
  min-width: 4rem;
  font-size: var(--ui-text-caption);
  color: var(--ui-ink-muted);
  background: var(--ui-surface);
  border: 1px solid var(--ui-edge);
  border-radius: var(--ui-radius-md);
}

.switch-toggle[aria-pressed="true"] {
  color: var(--ui-ink);
  background: var(--ui-accent);
  border-color: var(--ui-accent);
}

.switch-toggle:focus-visible,
.switch-lock:focus-visible {
  outline: 2px solid var(--ui-select);
  outline-offset: 1px;
}

.switch-lock {
  font-size: var(--ui-text-caption);
  color: var(--ui-ink-muted);
}

.switch-lock.by-gm {
  color: var(--ui-warning);
}

.switch-keys {
  display: flex;
  flex-wrap: wrap;
  gap: var(--ui-space-1) var(--ui-space-4);
  margin: 0;
  font-size: var(--ui-text-caption);
  color: var(--ui-ink-muted);
}

.switch-keys kbd {
  padding: 0 var(--ui-space-1);
  font: 600 0.7rem / 1.3 var(--ui-font-data);
  color: var(--ui-ink);
  background: var(--ui-surface);
  border: 1px solid var(--ui-edge);
  border-bottom-width: 2px;
  border-radius: var(--ui-radius-sm);
}

/* [/one view, part B] */

/* [relationship maps] ------------------------------------------------- */

/* The maps' window (relationship maps, decision 5) stands in the frame
   the main menu and Prep share (`map_window.rs`, the drawers and windows
   region below): only what is Relations' own is here. */
.rel-window {
  font-family: var(--ui-font-data);
}
.rel-body {
  display: grid;
  grid-template-columns: 15rem minmax(0, 1fr) 21rem;
  min-height: 0;
}
.rel-window .eyebrow,
.rel-list h3 {
  margin: 0;
  font-size: var(--ui-text-caption);
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--ui-ink-muted);
}
.rel-window kbd {
  padding: 0 var(--ui-space-1);
  font: 600 0.7rem / 1.3 var(--ui-font-data);
  color: var(--ui-ink);
  background: var(--ui-surface);
  border: 1px solid var(--ui-edge);
  border-bottom-width: 2px;
  border-radius: var(--ui-radius-sm);
}
.rel-window button {
  font: inherit;
  color: var(--ui-ink);
  background: var(--ui-surface-raised);
  border: 1px solid var(--ui-edge);
  border-radius: var(--ui-radius-md);
  padding: var(--ui-space-1) var(--ui-space-2);
  cursor: pointer;
}
.rel-window button:hover,
.rel-window button:focus-visible {
  border-color: var(--ui-select);
}
.rel-window button.danger {
  color: var(--ui-error);
}
.rel-window input[type="text"],
.rel-window textarea {
  box-sizing: border-box;
  width: 100%;
  font: inherit;
  color: var(--ui-ink);
  background: var(--ui-surface);
  border: 1px solid var(--ui-edge);
  border-radius: var(--ui-radius-md);
  padding: var(--ui-space-1) var(--ui-space-2);
}

/* The list of maps, on the left. */
.rel-list {
  display: flex;
  flex-direction: column;
  gap: var(--ui-space-2);
  padding: var(--ui-space-3);
  border-right: 1px solid var(--ui-edge);
  background: color-mix(in srgb, var(--ui-surface) 60%, transparent);
  min-height: 0;
}
.rel-list ul {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: var(--ui-space-1);
}
.rel-window .rel-map {
  display: grid;
  grid-template-columns: 1.6rem minmax(0, 1fr) auto;
  align-items: center;
  gap: var(--ui-space-2);
  width: 100%;
  text-align: left;
  background: none;
  border-color: transparent;
  border-left: 3px solid transparent;
}
.rel-window .rel-map.current {
  border-left-color: var(--ui-select);
  background: var(--ui-surface-raised);
}
.rel-map-name {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.rel-map.unreadable .rel-map-name,
.rel-map small {
  color: var(--ui-warning);
}
.rel-keys {
  margin-top: auto;
  display: grid;
  gap: var(--ui-space-1);
  color: var(--ui-ink-muted);
  font-size: var(--ui-text-caption);
}

/* The map itself. */
.rel-canvas {
  position: relative;
  overflow: hidden;
  cursor: grab;
  touch-action: none;
  user-select: none;
}
.rel-canvas.relating {
  cursor: crosshair;
}
.rel-svg {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
}
.rel-canvas-note,
.rel-relating {
  position: absolute;
  left: 50%;
  transform: translateX(-50%);
  margin: 0;
  padding: var(--ui-space-1) var(--ui-space-3);
  color: var(--ui-ink);
  background: var(--ui-surface-raised);
  border: 1px solid var(--ui-edge);
  border-radius: var(--ui-radius-md);
  pointer-events: none;
}
.rel-canvas-note {
  top: 45%;
}
.rel-relating {
  top: var(--ui-space-3);
  border-color: var(--ui-select);
}
.rel-edge {
  cursor: pointer;
}
.rel-edge-hit {
  fill: none;
  stroke: transparent;
  stroke-width: 14;
}
.rel-edge-line {
  fill: none;
  stroke-width: 2.5;
}
.rel-edge.chosen .rel-edge-line {
  stroke-width: 4.5;
}
.rel-pill {
  fill: var(--ui-surface);
  stroke-width: 1.5;
}
.rel-edge.chosen .rel-pill {
  stroke: var(--ui-select);
  stroke-width: 2.5;
}
.rel-pill-text {
  fill: var(--ui-ink);
  font-size: 11px;
  text-anchor: middle;
}
.rel-node {
  cursor: pointer;
}
.rel-ring {
  fill: none;
  stroke: none;
}
.rel-node.chosen .rel-ring {
  stroke: var(--ui-select);
  stroke-width: 3;
}
.rel-node.source .rel-ring,
.rel-node.target .rel-ring {
  stroke: var(--ui-select);
  stroke-width: 2;
  stroke-dasharray: 4 4;
}
.rel-node.target .rel-ring {
  stroke-dasharray: none;
}
.rel-disc {
  stroke: var(--ui-surface);
  stroke-width: 2;
}
.rel-initials {
  font: 700 14px var(--ui-font-data);
  text-anchor: middle;
}
.rel-name,
.rel-free-text {
  fill: var(--ui-ink);
  font-size: 12px;
  text-anchor: middle;
  paint-order: stroke;
  stroke: var(--ui-surface);
  stroke-width: 3px;
}
.rel-free {
  fill: color-mix(in srgb, var(--ui-surface-raised) 85%, transparent);
  stroke: var(--ui-ink-muted);
  stroke-width: 1.5;
  stroke-dasharray: 5 4;
}

/* The selected node or relation, on the right. */
.rel-panel {
  padding: var(--ui-space-3);
  border-left: 1px solid var(--ui-edge);
  background: color-mix(in srgb, var(--ui-surface) 60%, transparent);
  overflow: auto;
  min-height: 0;
}
.rel-panel > div {
  display: grid;
  gap: var(--ui-space-2);
  align-content: start;
}
.rel-panel h3 {
  margin: 0;
  font: 600 var(--ui-text-lead) / 1.25 var(--ui-font-prose);
}
.rel-panel h4 {
  margin: var(--ui-space-2) 0 0;
  font-size: var(--ui-text-small);
  color: var(--ui-ink-muted);
}
.rel-panel label {
  font-size: var(--ui-text-small);
  color: var(--ui-ink-muted);
}
.rel-counts,
.rel-hint {
  margin: 0;
  color: var(--ui-ink-muted);
  font-size: var(--ui-text-small);
}
.rel-chips {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--ui-space-1);
}
.rel-window .rel-chip {
  display: flex;
  align-items: center;
  gap: var(--ui-space-2);
  text-align: left;
}
.rel-window .rel-chip.chosen {
  border-color: var(--ui-select);
  background: var(--ui-surface);
}
.rel-swatch {
  display: inline-block;
  width: 0.7rem;
  height: 0.7rem;
  flex: none;
  border-radius: 50%;
}
.rel-directed {
  display: flex;
  gap: var(--ui-space-2);
  align-items: center;
}
.rel-list-rows,
.rel-rows {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: var(--ui-space-1);
}
.rel-window .rel-row {
  display: flex;
  align-items: center;
  gap: var(--ui-space-2);
  width: 100%;
  text-align: left;
  background: none;
}
.rel-rows li {
  padding: var(--ui-space-1) var(--ui-space-2);
  border-radius: var(--ui-radius-md);
  cursor: pointer;
}
.rel-rows li.current {
  background: var(--ui-surface-raised);
  outline: 1px solid var(--ui-select);
}
.rel-rows li.free {
  font-style: italic;
}
.rel-confirm {
  display: grid;
  gap: var(--ui-space-2);
}
.rel-confirm p {
  margin: 0;
}
.data-rail .rail-door .label {
  font-style: italic;
}
/* Part B: a relation the GM revealed, drawn on a player's map as known:
   a pale halo under its line, and its pill ringed in the selection gold. */
.rel-edge.known .rel-edge-line {
  filter: drop-shadow(0 0 3px var(--ui-select));
}
.rel-edge.known .rel-pill {
  stroke: var(--ui-select);
}
.rel-known-type {
  display: flex;
  align-items: center;
  gap: var(--ui-space-2);
  margin: 0;
}
/* The players' maps under the GM's own, and the GM's Revealed to. */
.rel-players {
  display: grid;
  gap: var(--ui-space-1);
  padding-top: var(--ui-space-2);
  border-top: 1px solid var(--ui-edge);
}
.rel-players h3 {
  margin: 0;
  font-size: var(--ui-text-caption);
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--ui-ink-muted);
}
.rel-seat {
  margin: var(--ui-space-1) 0 0;
  font-size: var(--ui-text-small);
  color: var(--ui-ink-muted);
}
.rel-reveal {
  display: grid;
  gap: var(--ui-space-1);
  padding-top: var(--ui-space-2);
  border-top: 1px solid var(--ui-edge);
}
.rel-reveal h4 {
  margin: 0;
}
.rel-reveal-seats {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: var(--ui-space-1);
}
.rel-reveal-seats li {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--ui-space-2);
}
.rel-unseen {
  color: var(--ui-warning);
}
/* [/relationship maps] ------------------------------------------------ */

/* [drawers and windows A: resizable drawers, the poker table] --------- */

/* A window resizes by its grips (decision 1, drawers.rs's DrawerWindow):
   its right edge, its bottom edge and the corner between, never the
   browser's own corner, which reached only the corner. The grips sit
   inside the box, on its edges, over what the panel draws there. */
.drawer-window {
  resize: none;
}

.drawer-grip {
  position: absolute;
  z-index: 2;
  touch-action: none;
}

.drawer-grip[data-grip="right"] {
  top: 0;
  right: 0;
  bottom: 0.9rem;
  width: 6px;
  cursor: ew-resize;
}

.drawer-grip[data-grip="bottom"] {
  left: 0;
  right: 0.9rem;
  bottom: 0;
  height: 6px;
  cursor: ns-resize;
}

.drawer-grip[data-grip="corner"] {
  right: 0;
  bottom: 0;
  width: 0.9rem;
  height: 0.9rem;
  cursor: nwse-resize;
  background:
    linear-gradient(135deg, transparent 55%, var(--ui-ink-muted) 55% 62%, transparent 62% 74%, var(--ui-ink-muted) 74% 81%, transparent 81%);
  opacity: 0.6;
}

.drawer-window:is(:hover, :focus-within) .drawer-grip[data-grip="corner"] {
  opacity: 1;
}

.drawer-grip:hover,
.drawer-window.resizing .drawer-grip {
  background-color: color-mix(in srgb, var(--ui-accent) 40%, transparent);
}

.drawer-window:is([data-afloat="false"], .collapsed) .drawer-grip {
  display: none;
}

.drawer-window.resizing {
  user-select: none;
}

/* The Table popped out as a poker table (decisions 2 and 8, table_ui.rs's
   PokerTable): a stadium of padded rail, wood and felt with a betting
   line, the dealer's seat at the top, the players' chips on the rail.
   The felt and the wood are the night palette's own green and orange
   token colours, set on the table from theme.rs. The chips sit on the
   rail's box by percent, so the inset that holds them is the table's
   padding. */
.poker {
  --chip: 2.6rem;
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--ui-space-3);
  align-items: start;
}

.poker > * {
  min-width: 0;
}

.poker[data-felt="beside"] {
  grid-template-columns: minmax(0, 1fr) minmax(16rem, 24rem);
}

.poker-table {
  position: relative;
  padding: calc(var(--chip) / 2 + var(--ui-space-1)) calc(var(--chip) / 2 + var(--ui-space-2))
    calc(var(--chip) / 2 + 1.1rem);
}

.poker-rail {
  aspect-ratio: 2 / 1;
  box-sizing: border-box;
  padding: 0.55rem;
  border-radius: 9999px;
  background: color-mix(in srgb, var(--ui-surface-raised) 65%, black);
  box-shadow:
    inset 0 2px 5px rgb(255 255 255 / 0.06),
    inset 0 -3px 8px rgb(0 0 0 / 0.6),
    0 8px 20px rgb(0 0 0 / 0.5);
}

.poker-wood {
  height: 100%;
  box-sizing: border-box;
  padding: 0.4rem;
  border-radius: 9999px;
  background: linear-gradient(
    color-mix(in srgb, var(--poker-wood) 85%, var(--ui-ink)),
    color-mix(in srgb, var(--poker-wood) 70%, black)
  );
}

.poker-felt {
  position: relative;
  height: 100%;
  border-radius: 9999px;
  background: radial-gradient(
    ellipse at center,
    color-mix(in srgb, var(--poker-felt) 88%, var(--ui-ink)),
    var(--poker-felt) 55%,
    color-mix(in srgb, var(--poker-felt) 70%, black)
  );
  box-shadow: inset 0 0 18px rgb(0 0 0 / 0.55);
}

.poker-line {
  position: absolute;
  inset: 20% 12%;
  border: 1px dashed color-mix(in srgb, var(--ui-ink) 35%, transparent);
  border-radius: 9999px;
}

.poker-seats,
.poker-centre {
  position: absolute;
  inset: calc(var(--chip) / 2 + var(--ui-space-1)) calc(var(--chip) / 2 + var(--ui-space-2))
    calc(var(--chip) / 2 + 1.1rem);
  margin: 0;
  padding: 0;
}

.poker-seats {
  list-style: none;
  pointer-events: none;
}

.poker-centre {
  display: grid;
  place-content: center;
  gap: var(--ui-space-1);
  text-align: center;
  pointer-events: none;
}

.poker-centre p {
  margin: 0;
}

.poker-scene {
  font-family: var(--ui-font-prose);
  font-variant: small-caps;
  letter-spacing: 0.04em;
  color: var(--ui-ink);
  text-shadow: 0 1px 2px rgb(0 0 0 / 0.7);
}

.poker-clock {
  display: grid;
  color: var(--ui-ink);
  text-shadow: 0 1px 2px rgb(0 0 0 / 0.7);
}

.poker-time {
  font-family: var(--ui-font-data);
  font-size: var(--ui-text-title);
  font-variant-numeric: tabular-nums;
}

.poker-date {
  font-size: var(--ui-text-caption);
  color: var(--ui-ink-muted);
}

.poker-seat {
  position: absolute;
  left: var(--chip-x);
  top: var(--chip-y);
  translate: -50% -50%;
  pointer-events: auto;
}

/* The dealer's seat, at the rail's top (decision 2). The chip look below
   it shares with the players places nothing, so it keeps this place. */
.poker-table > .poker-dealer {
  --face-fill: var(--ui-ink);
  --face-ink: var(--ui-surface);
  position: absolute;
  left: 50%;
  top: calc(var(--chip) / 2 + var(--ui-space-1));
  translate: -50% -50%;
}

.poker-chip {
  position: relative;
}

:is(.poker-chip, .poker-dealer) {
  display: grid;
  place-items: center;
  width: var(--chip);
  height: var(--chip);
  box-sizing: border-box;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: repeating-conic-gradient(var(--face-fill) 0 22deg, var(--face-ink) 22deg 30deg);
  color: var(--face-ink);
  font: 700 var(--ui-text-small) / 1 var(--ui-font-data);
  box-shadow: 0 3px 6px rgb(0 0 0 / 0.55);
  cursor: pointer;
}

.poker-dealer {
  cursor: default;
}

:is(.poker-chip, .poker-dealer) .chip-face {
  display: grid;
  place-items: center;
  width: 74%;
  height: 74%;
  border-radius: 50%;
  background: var(--face-fill);
  outline: 1px dashed color-mix(in srgb, var(--face-ink) 55%, transparent);
  outline-offset: -3px;
}

.poker-chip[aria-pressed="true"] {
  box-shadow:
    0 0 0 2px var(--ui-surface),
    0 0 0 4px var(--ui-select),
    0 3px 6px rgb(0 0 0 / 0.55);
}

.poker-chip:focus-visible {
  outline: 2px solid var(--ui-select);
  outline-offset: 3px;
}

.poker-chip .chip-dot {
  position: absolute;
  top: 0;
  right: 0;
  width: 0.6rem;
  height: 0.6rem;
  box-sizing: border-box;
  border-radius: 50%;
  border: 2px solid var(--ui-edge);
  background: var(--ui-surface);
}

.poker-chip .chip-dot[data-glance="online"] {
  background: var(--table-online);
  border-color: var(--table-online);
}

.poker-chip .chip-dot[data-glance="open"] {
  border-color: var(--ui-accent);
}

.poker-chip .chip-digit {
  position: absolute;
  bottom: -0.15rem;
  left: -0.15rem;
  min-width: 1rem;
  padding: 0.1rem 0.2rem;
  box-sizing: border-box;
  border: 1px solid var(--ui-edge);
  border-radius: var(--ui-radius-sm);
  background: var(--ui-surface);
  color: var(--ui-ink-muted);
  font: var(--ui-text-caption) / 1 var(--ui-font-data);
}

:is(.poker-chip, .poker-dealer) .chip-name {
  position: absolute;
  top: calc(100% + 0.15rem);
  left: 50%;
  translate: -50% 0;
  max-width: 7rem;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  color: var(--ui-ink);
  font-weight: 600;
  font-size: var(--ui-text-caption);
  text-shadow: 0 1px 2px rgb(0 0 0 / 0.8);
}

.poker-dealer .chip-name {
  top: auto;
  bottom: calc(100% + 0.15rem);
  color: var(--ui-ink-muted);
}

.poker-chip .chip-glance {
  display: none;
}

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

/* Narrow (under 480 px), the seats fall back to rows: the felt's scene and
   clock a line over them, no rail, no dealer. */
.poker[data-felt="rows"] .poker-table {
  padding: 0;
}

.poker[data-felt="rows"] :is(.poker-rail, .poker-dealer) {
  display: none;
}

.poker[data-felt="rows"] :is(.poker-seats, .poker-centre) {
  position: static;
  inset: auto;
}

.poker[data-felt="rows"] .poker-centre {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  align-items: baseline;
  margin-bottom: var(--ui-space-2);
  text-align: start;
}

.poker[data-felt="rows"] .poker-clock {
  display: flex;
  gap: var(--ui-space-2);
  align-items: baseline;
}

.poker[data-felt="rows"] .poker-time {
  font-size: var(--ui-text-body);
}

.poker[data-felt="rows"] .poker-seats {
  display: grid;
  gap: var(--ui-space-1);
  pointer-events: auto;
}

.poker[data-felt="rows"] .poker-seat {
  position: static;
  translate: none;
}

.poker[data-felt="rows"] .poker-chip {
  display: flex;
  gap: var(--ui-space-2);
  align-items: center;
  width: 100%;
  height: auto;
  padding: var(--ui-space-1) var(--ui-space-2);
  border: 1px solid var(--ui-edge);
  border-radius: var(--ui-radius-md);
  background: var(--ui-surface);
  color: var(--ui-ink);
  font-weight: 400;
  box-shadow: none;
  text-align: start;
}

.poker[data-felt="rows"] .poker-chip[aria-pressed="true"] {
  border-color: var(--ui-select);
}

.poker[data-felt="rows"] .poker-chip .chip-face {
  flex: none;
  width: 1.6rem;
  height: 1.6rem;
  color: var(--face-ink);
  font-weight: 700;
}

.poker[data-felt="rows"] .poker-chip :is(.chip-dot, .chip-digit, .chip-name) {
  position: static;
  translate: none;
}

.poker[data-felt="rows"] .poker-chip .chip-name {
  flex: 1;
  min-width: 0;
  max-width: none;
  text-shadow: none;
  font-size: var(--ui-text-small);
}

.poker[data-felt="rows"] .poker-chip .chip-digit {
  order: 4;
}

.poker[data-felt="rows"] .poker-chip .chip-glance {
  display: inline;
  order: 3;
  color: var(--ui-ink-muted);
  font-size: var(--ui-text-caption);
}

/* The picked player's card under the rows fits their width: its actions
   take a line of their own and wrap. */
.poker[data-felt="rows"] .poker-card .table-seat {
  grid-template-columns: auto minmax(0, 1fr);
}

.poker[data-felt="rows"] .poker-card .table-actions {
  grid-column: 1 / -1;
  flex-wrap: wrap;
}

/* A popped-out window scrolls only what cannot fit, and shows no bar
   until the pointer is over it (the standing preference). */
.drawer-window .fan-panel {
  scrollbar-width: thin;
  scrollbar-color: transparent transparent;
}

.drawer-window .fan-panel:hover {
  scrollbar-color: var(--ui-edge) transparent;
}

/* [/drawers and windows A] -------------------------------------------- */

/* [drawers and windows B: condensed Characters, the drag disc] -------- */

/* The Characters drawer condensed (decision 3, layout A): wide enough
   for its directories side by side, each character on one line, so the
   whole cast fits with no scroll bar; docked only, a popped-out window
   keeping its own size. */
.drawer[data-slot="top"] .drawer-fan:has(.characters-drawer) {
  width: min(42rem, calc(100vw - var(--top-right) - var(--ui-space-2)));
}

/* The header: one name, then New NPC, New character and New group. */
.characters-drawer .characters-new {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--ui-space-1);
  margin: 0 0 var(--ui-space-2);
}

.characters-drawer .characters-new input {
  flex: 1 1 8rem;
  min-width: 0;
}

.characters-drawer .characters-new button {
  font-size: var(--ui-text-small);
}

/* The directories, in as many columns as fit. */
.characters-columns {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(12.5rem, 1fr));
  gap: var(--ui-space-2) var(--ui-space-3);
  align-items: start;
}

.characters-columns .characters-section + .characters-section {
  margin-top: 0;
}

.characters-columns .characters-heading {
  padding-bottom: 2px;
  border-bottom: 1px solid var(--ui-edge);
  font-size: var(--ui-text-small);
}

/* A group's Delete shows while its heading is picked or hovered. */
.characters-columns .characters-heading .top-door {
  visibility: hidden;
}

.characters-columns .characters-heading:is(:focus-within, :hover) .top-door {
  visibility: visible;
}

.characters-columns .characters-list {
  margin: var(--ui-space-1) 0 0;
  gap: 1px;
}

/* A row: disc, name, status icons, kind, on one line. */
.characters-columns .characters-list li {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--ui-space-1);
  padding: 1px var(--ui-space-1);
  border-color: transparent;
}

.characters-columns .characters-list li.focused {
  border-color: var(--ui-accent);
}

.characters-columns .characters-list li:focus-within {
  background: var(--ui-surface-raised);
}

.characters-columns .characters-pick {
  flex: 1 1 0;
  min-width: 0;
}

.characters-columns .characters-pick .name {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: var(--ui-text-small);
}

.characters-columns .characters-list .kind {
  display: inline;
  font-size: var(--ui-text-caption);
}

/* Status as icons, the word in the hover; a status this build does not
   know keeps its word. */
.characters-columns .status-badges {
  flex-wrap: nowrap;
  gap: 2px;
  margin-left: 0;
}

.characters-columns .status-badges .badge:is([data-status="wounded"], [data-status="hidden"], [data-status="frenzied"], [data-status="staked"], [data-status="impaired"]) {
  display: inline-grid;
  place-items: center;
  width: 1.1rem;
  height: 1.1rem;
  padding: 0;
  border-radius: 50%;
  font-size: 0;
}

.characters-columns .status-badges .badge::before {
  font-size: var(--ui-text-caption);
  line-height: 1;
}

.characters-columns .badge[data-status="wounded"]::before {
  content: "✚";
}

.characters-columns .badge[data-status="hidden"]::before {
  content: "◌";
}

.characters-columns .badge[data-status="frenzied"]::before {
  content: "✷";
}

.characters-columns .badge[data-status="staked"]::before {
  content: "†";
}

.characters-columns .badge[data-status="impaired"]::before {
  content: "▾";
}

/* A row's actions, on the picked row only: the row the keys stand in,
   else the focused character's. */
.characters-columns .characters-actions {
  display: none;
  flex-basis: 100%;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--ui-space-1);
  padding: 0 0 var(--ui-space-1) 1.5rem;
}

.characters-columns .characters-row:focus-within .characters-actions,
.characters-drawer:not(:has(.characters-row:focus-within)) .characters-row.focused .characters-actions {
  display: flex;
}

.characters-actions .top-door,
.characters-actions .characters-group,
.characters-actions .status-control {
  margin: 0;
  font-size: var(--ui-text-caption);
}

/* The drag disc (decision 5): a Characters row's token, and the
   Character drawer's head. */
.character-disc {
  width: 1.25rem;
  height: 1.25rem;
  font-size: 0.6rem;
}

.character-disc[draggable="true"] {
  cursor: grab;
}

.character-disc[draggable="true"]:active {
  cursor: grabbing;
}

.character-disc.riding {
  cursor: not-allowed;
  opacity: 0.55;
}

.gm-disc-pane {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  gap: var(--ui-space-2);
  align-items: start;
}

.gm-disc-pane > .character-disc {
  width: 1.6rem;
  height: 1.6rem;
  font-size: var(--ui-text-caption);
}
/* [/drawers and windows B] -------------------------------------------- */

/* [drawers and windows C: the menu and Prep over the map] ------------- */

/* The window over the map (decisions 4 and 10, `map_window.rs`), the
   frame the main menu, Prep and Relations share: near-full-screen under
   the bar, its ground a see-through surface so the map shows below — a
   fixed amount, never a preference. While one is shown the drawers and
   their tabs, and the map's own UI, hide; they keep their boxes, so the
   camera measures nothing new when they come back. The map stays live
   behind but takes no pointer. A window kept mounted while shut (Prep)
   is `.shut`, and hides nothing. */
:root:has(.map-window:not(.shut))
  :is(.drawer-layer, .map-dock, .map-tools, .view-menu, .map-status-stack, .claim-pending,
      .maplibregl-control-container) {
  visibility: hidden;
}
:root:has(.map-window:not(.shut)) .map-stage {
  pointer-events: none;
}
.map-window {
  position: fixed;
  top: calc(var(--ui-chrome-top) + var(--save-banner-h) + var(--ui-space-3));
  left: var(--ui-space-4);
  right: var(--ui-space-4);
  bottom: var(--ui-space-4);
  z-index: var(--ui-z-menu);
  box-sizing: border-box;
  display: grid;
  grid-template-rows: auto minmax(0, 1fr);
  padding: 0;
  overflow: hidden;
  color: var(--ui-ink);
  background: color-mix(in srgb, var(--ui-surface) 78%, transparent);
  border: 1px solid var(--ui-edge);
  border-radius: var(--ui-radius-lg);
  box-shadow: var(--ui-elevation-2);
}
.map-window.shut {
  display: none;
}
.map-window:focus {
  outline: none;
}
.map-window:focus-visible {
  outline: 2px solid var(--ui-select);
  outline-offset: -2px;
}
.map-window-head {
  display: flex;
  align-items: baseline;
  gap: var(--ui-space-4);
  padding: var(--ui-space-2) var(--ui-space-4);
  border-bottom: 1px solid var(--ui-edge);
}
.map-window-head h2 {
  margin: 0;
  font: 600 var(--ui-text-lead) / 1.2 var(--ui-font-prose);
}
.map-window-status {
  flex: 1;
  color: var(--ui-ink-muted);
  font-size: var(--ui-text-small);
}
.map-window-head .close {
  padding: 0 var(--ui-space-1);
  background: none;
  border: none;
  color: var(--ui-ink-muted);
  font-size: var(--ui-text-title);
  cursor: pointer;
}
.map-window-head .close:hover,
.map-window-head .close:focus-visible {
  color: var(--ui-ink);
}

/* The main menu in it (decision 4): its choices in a column at the
   window's middle. The menu's older dialog rules (wave F's region) set a
   centred box; the window's frame takes their place. */
.map-window.home-menu {
  width: auto;
  transform: none;
  gap: 0;
}
.home-body {
  display: grid;
  align-content: center;
  justify-items: center;
  gap: var(--ui-space-5);
  min-height: 0;
  overflow-y: auto;
  padding: var(--ui-space-6) var(--ui-space-4);
}
.home-body > * {
  box-sizing: border-box;
  width: min(30rem, 100%);
}

/* Prep in it (decision 4): its rail beside the open book, below the
   window's heading. Prep is still `.data-overlay`, whose older rules
   (opaque, edge to edge, padded) the frame's replace; it keeps the
   overlay's layer, under the drawers' and the palette's. */
.map-window.data-overlay {
  z-index: var(--ui-z-scrim);
}
:root:has(.topbar .status.error) .map-window.data-overlay {
  border-top: 1px solid var(--ui-edge);
}
.prep-body {
  display: grid;
  grid-template-columns: 12.5rem minmax(0, 1fr);
  min-height: 0;
}
@media (max-width: 860px) {
  .prep-body {
    grid-template-columns: 3.6rem minmax(0, 1fr);
  }
}
/* A check the GM offered a seat is never hidden by a window: the offer
   cards, last in the drawer layer, stay shown while the drawers hide,
   and the layer rises over the window (it takes no click itself, and
   its hidden drawers take none), so a seat with Relations open still
   sees and answers them. The GM's page needs no such rule: what the
   players send reaches the bar's Inbox badge, which no window hides. */
:root:has(.map-window:not(.shut)) .drawer-layer:has(.offer-stack) {
  z-index: calc(var(--ui-z-menu) + 1);
}
:root:has(.map-window:not(.shut)) .drawer-layer .offer-stack {
  visibility: visible;
}
/* [/drawers and windows C] -------------------------------------------- */
