/* ---- SOCIAL ------------------------------------------------------------------------------------
   Narrower than the ledgers, like the settings window: this is three short lists and two text
   boxes, and a name stranded on the far side of 880px of parchment reads as unrelated to the
   buttons that act on it. */
/* A FIXED BODY, so the window does not resize between tabs.
   The three views hold wildly different amounts: Friends is two lists and a box, Party is a member
   list, two rule toggles and a leave button, and the Blacklist is often empty — so the panel grew
   and shrank under the pointer every time a tab was clicked, which is the one motion an interface
   should never make in response to navigation.
   The height is on the BODY rather than the panel, so the title and the tab rail stay put and only
   the content scrolls. `min-height` and `max-height` at the same value: a short view is padded out,
   a long one scrolls inside it. */
.rpg-panel--social { width: min(560px, 92vw); }
.rpg-social-body { min-height: 420px; max-height: 420px; overflow-y: auto;
  /* The scrollbar takes width when it appears, and a body that reflows as a list crosses the
     threshold is the same resize by another route. Reserve the gutter always. */
  scrollbar-gutter: stable; }
/* The VIEW rail. One entry today (Friends); Party joins it, so it is a row of tabs from the start
   rather than a heading that would have to become one. */
.rpg-social-views { display: flex; gap: var(--sp-2); margin-bottom: var(--sp-4);
  border-bottom: 1px solid var(--glass-stroke); padding-bottom: var(--sp-2); }
.rpg-social-view { padding: 5px 14px; border-radius: var(--r-2); background: transparent;
  border: 1px solid transparent; color: var(--ink-soft);
  font-family: var(--font-banner); font-size: 12px; letter-spacing: .08em;
  text-transform: uppercase; cursor: pointer; }
.rpg-social-view:hover { color: var(--ink); }
.rpg-social-view.is-active { color: var(--gold-hi); border-color: rgba(201,164,110,.45);
  background: rgba(255,240,200,.06); }

.rpg-social-section { margin-bottom: var(--sp-4); }
.rpg-social-head { font-family: var(--font-banner); font-size: 12px; letter-spacing: .12em;
  text-transform: uppercase; color: var(--gold); margin: 0 0 var(--sp-2); }
/* An empty section is a DESCRIPTION, not an apology — quill italic, the voice the rest of the
   interface uses for an aside, rather than the red the interface uses for a refusal. */
.rpg-social-empty { font: italic 13px var(--font-quill); color: var(--ink-quill);
  margin: 0 0 var(--sp-2); }
.rpg-social-list { display: flex; flex-direction: column; gap: 4px; }
.rpg-social-row { display: flex; align-items: center; gap: var(--sp-3);
  padding: 7px var(--sp-3); border-radius: var(--r-2);
  border: 1px solid var(--glass-stroke); background: var(--glass-deep); }
/* min-width:0 plus the ellipsis is what stops a sixteen-character name (the maximum the name rule
   allows) from shoving Message and Remove off the end of the row. */
.rpg-social-name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis;
  white-space: nowrap; font: 600 14px var(--font-body); color: var(--ink); }
.rpg-social-presence { flex: none; font-family: var(--font-banner); font-size: 10px;
  letter-spacing: .1em; text-transform: uppercase; color: var(--ink-dim); }
/* A DOT as well as the word: colour alone is not a status anyone can read, and the word alone at
   10px is easy to skim past. */
.rpg-social-presence::before { content: '●'; margin-right: 5px; font-size: 9px; }
.rpg-social-presence.is-online { color: #8ef0a0; }
.rpg-social-acts { flex: none; display: flex; gap: 5px; }
.rpg-social-act { padding: 4px 10px; border-radius: var(--r-1, 5px);
  border: 1px solid var(--glass-stroke); background: transparent; color: var(--ink-soft);
  font-family: var(--font-banner); font-size: 10px; letter-spacing: .08em;
  text-transform: uppercase; cursor: pointer; white-space: nowrap; }
/* NOT WHILE IT IS REFUSED. A greyed control that still lights up under the cursor is telling the
   player two opposite things at once. */
.rpg-social-act:hover:not(:disabled) { color: var(--ink); border-color: var(--gold); }
.rpg-social-act:disabled { opacity: .5; cursor: not-allowed; }
.rpg-social-act.is-yes { color: #8ef0a0; border-color: rgba(142,240,160,.45); }
/* The ARMED confirm, and only it, is ember. Nothing else in this panel is red — a row of warnings
   teaches the player to ignore the one that matters. */
.rpg-social-act.is-danger { color: var(--ember); border-color: var(--ember); }

/* A BLOCKED ROW is three lines deep, so it wraps rather than squeezing. The name and Unblock share
   the first line; the three scope checkboxes take a full-width second line; the standing
   explanation takes a third. `flex: 1 0 100%` is the same device `.rpg-party-row .rpg-social-acts`
   already uses to push a second line inside one flex row. */
.rpg-block-row { flex-wrap: wrap; row-gap: var(--sp-2, 7px); }
.rpg-block-scopes { flex: 1 0 100%; display: flex; flex-wrap: wrap; gap: var(--sp-2, 7px) var(--sp-4, 14px); }
.rpg-block-scope { display: flex; align-items: center; gap: 6px; cursor: pointer;
  font: 12px var(--font-body); color: var(--ink-soft); white-space: nowrap; }
.rpg-block-scope:hover { color: var(--ink); }
/* The stock checkbox paints itself system-blue, the one colour nowhere in this interface, and no
   unit test can see it. `accent-color` recolours the native control and keeps its keyboard
   behaviour -- the same choice the trade window's Ready box makes, with the same sage. */
.rpg-block-scope input[type=checkbox] { accent-color: var(--sage); width: 14px;
  height: 14px; margin: 0; flex: none; cursor: pointer; }
/* ALWAYS PRESENT, never conditional: unticking all three loosens the channels but does NOT unblock,
   and a player who cleared the boxes with no explanation would conclude otherwise. Quill italic --
   the interface's voice for an aside -- not the ember it uses for a refusal.
   Explicit hex fallback: `--ink-dim` is NOT defined in the design system and resolves to black,
   which on this panel renders as an empty gap. `--ink-quill` IS defined; the fallback is belt. */
/* Sits under the BLOCKED heading, above the list -- not inside a row -- so it is read once. */
.rpg-block-stands { margin: 0 0 var(--sp-2, 7px); font: italic 12px var(--font-quill);
  color: var(--ink-quill); line-height: 1.45; }

.rpg-social-form { margin: 0 0 var(--sp-5); }
.rpg-social-form-label { display: block; font-family: var(--font-banner); font-size: 11px;
  letter-spacing: .1em; text-transform: uppercase; color: var(--ink-soft); margin-bottom: 5px; }
.rpg-social-form-row { display: flex; gap: var(--sp-2); }
.rpg-social-input { flex: 1; min-width: 0; padding: 8px 11px; border-radius: var(--r-2);
  border: 1px solid var(--glass-stroke); background: var(--glass-deep); color: var(--ink);
  font: 14px var(--font-body); outline: none; }
.rpg-social-input:focus { border-color: var(--gold); }
.rpg-social-input::placeholder { color: var(--ink-dim); }
.rpg-social-submit { flex: none; padding: 0 16px; border-radius: var(--r-2);
  border: 1px solid var(--gold); background: rgba(255,240,200,.06); color: var(--gold-hi);
  font-family: var(--font-banner); font-size: 11px; letter-spacing: .1em;
  text-transform: uppercase; cursor: pointer; }
.rpg-social-submit:hover:not(:disabled) { background: rgba(255,240,200,.14); }
/* Greyed, and the rule sits underneath it in words — see NAME_HINT. A disabled control with no
   explanation is one the player concludes is broken. */
.rpg-social-submit:disabled { opacity: .38; cursor: not-allowed; border-color: var(--glass-stroke);
  color: var(--ink-dim); }
.rpg-social-hint { font: italic 12px var(--font-quill); color: var(--ink-quill);
  margin: 5px 0 0; }

/* THE TOOLBAR DOT. Markup on every nav button, painted on none of them until the button carries
   `.has-notice` — see mkButton. */
.rpg-actionbar .rpg-btn { position: relative; }
.rpg-btn-dot { display: none; position: absolute; top: 4px; right: 6px;
  width: 9px; height: 9px; border-radius: 50%; background: var(--ember, #e8663c);
  box-shadow: 0 0 0 2px rgba(13,10,32,.9), 0 0 8px rgba(232,102,60,.8); }
.rpg-actionbar .rpg-btn.has-notice .rpg-btn-dot { display: block; }

/* THE RIGHT-CLICK MENU (ContextMenu). Fixed, not absolute: it is placed from clientX/clientY, and
   an absolutely-positioned copy would drift by the HUD container's own offset.
   z-index 70 puts it over every panel and toast — it is modal in intent while it is up, and a menu
   half-buried under a notice is a menu the player cannot finish using. */
.rpg-ctx { position: fixed; z-index: 70; min-width: 148px; max-width: 260px;
  display: flex; flex-direction: column; padding: 4px;
  border: 1px solid var(--glass-stroke); border-radius: var(--r-2, 8px);
  background: var(--glass-deep, rgba(18,16,22,.94));
  box-shadow: 0 10px 28px rgba(0,0,0,.5); }
/* WHO the menu is about. Two players standing together are two overlapping figures at this
   camera; the verbs alone do not say which one was picked. */
.rpg-ctx-header { padding: 6px 10px 7px; margin-bottom: 3px;
  border-bottom: 1px solid var(--glass-stroke);
  font-family: var(--font-banner); font-size: 11px; letter-spacing: .1em;
  text-transform: uppercase; color: var(--gold-hi, #e8d6ac);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.rpg-ctx-item { padding: 7px 10px; border: 0; border-radius: var(--r-1, 5px);
  background: transparent; color: var(--ink-soft); text-align: left;
  font-family: var(--font-banner); font-size: 11px; letter-spacing: .07em;
  text-transform: uppercase; cursor: pointer; white-space: nowrap;
  overflow: hidden; text-overflow: ellipsis; }
.rpg-ctx-item:hover:not(:disabled) { background: rgba(255,240,200,.09); color: var(--ink); }
/* The armed confirm, and only it, is ember — the same single-warning rule the Social panel keeps.
   A menu where several rows look dangerous teaches the player to ignore the one that is. */
.rpg-ctx-item.is-danger { color: var(--ember, #e8663c); }
.rpg-ctx-item.is-danger:hover:not(:disabled) { background: rgba(232,102,60,.14); }
/* Greyed, and it always carries a `title` saying why — see ContextMenuItem.title.
   THE FALLBACK IS LOAD-BEARING: `--ink-dim` is not defined anywhere in the design system, so
   `var(--ink-dim)` alone resolves to the initial colour — BLACK — and at .38 opacity on this
   near-black panel the entry rendered as an empty gap. It passed its unit test (the button is
   there and is disabled) and was only visible as a defect in a screenshot. Dim enough to read as
   dead, opaque enough to READ. */
.rpg-ctx-item:disabled { opacity: .8; cursor: not-allowed; color: var(--ink-dim, #8b83a0); }

/* ---- TRADE -------------------------------------------------------------------------------------
   Two tables side by side, and a warning strip between the title and them. Not an `.rpg-panel`:
   the trade window is not a toolbar overlay, it cannot be opened by the player, and it must stay
   readable with the bag open BEHIND it — the drag from bag to table crosses both. */
.rpg-trade { position: fixed; inset: 0; display: none; align-items: center; justify-content: center;
  z-index: 60; pointer-events: none; }
.rpg-trade.is-open { display: flex; }
.rpg-trade-window { pointer-events: auto; width: min(760px, 94vw);
  padding: var(--sp-4, 14px) var(--sp-5, 18px) var(--sp-4, 14px);
  border-radius: var(--r-3, 10px); border: 1px solid var(--glass-stroke, rgba(255,255,255,.10));
  background: var(--chrome, rgba(13,10,34,.93));
  box-shadow: 0 18px 50px rgba(0,0,0,.55), var(--frame-inner-glow, none); }
.rpg-trade-title { font-family: var(--font-banner); font-size: 13px; letter-spacing: .14em;
  text-transform: uppercase; color: var(--gold); margin-bottom: var(--sp-3, 10px); }

/* THE ANTI-SCAM BANNER. Hidden entirely when quiet — an always-present strip is furniture, and
   furniture is not read. Ember, and the only ember in this window, so the one warning that matters
   is the one thing shouting. */
.rpg-trade-alert { display: none; margin-bottom: var(--sp-3, 10px);
  padding: 8px 11px; border-radius: var(--r-2, 7px);
  border: 1px solid var(--ember); background: var(--ember-soft, rgba(255,122,82,.18));
  color: var(--ember); font: 600 13px var(--font-body); }
.rpg-trade-alert.is-on { display: block; }

.rpg-trade-cols { display: grid; grid-template-columns: 1fr 1fr; gap: var(--sp-3, 10px); }
.rpg-trade-side { padding: var(--sp-3, 10px); border-radius: var(--r-2, 7px);
  border: 1px solid var(--glass-stroke, rgba(255,255,255,.10));
  background: var(--glass-deep, rgba(9,7,26,.78)); }
.rpg-trade-who { margin: 0 0 var(--sp-2, 7px); font: 600 14px var(--font-body);
  color: var(--gold-hi); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* TILES, NOT ROWS (owner's call). An item is a square with an icon and a corner count everywhere
   else in the game; the trade table was the one place it turned into a line of text, which made
   comparing two offers a reading job instead of a looking one. Four across at 12 slots gives three
   rows, and the grid holds that height whether it is full or empty so nothing reflows under a hand
   about to press Confirm. */
.rpg-trade-lines { display: flex; flex-direction: column; gap: 4px; min-height: 132px; }
.rpg-trade-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: var(--sp-2, 7px); align-content: start; min-height: 0; position: relative; }
.rpg-trade-grid .rpg-cell { cursor: default; }
.rpg-trade-grid button.rpg-cell { cursor: pointer; }
/* The empty half's sentence, over the top of its own blank squares — the squares say "twelve fit",
   the line says what to do about it, and neither needs the other's space. */
.rpg-trade-grid.is-empty::after { content: attr(data-empty); position: absolute; inset: 0;
  display: grid; place-items: center; pointer-events: none; padding: 0 8px; text-align: center;
  font: italic 12px var(--font-quill); color: var(--ink-quill); }
.rpg-trade-grid.is-empty .rpg-cell { opacity: .35; }

/* YOUR BAG, under the table. Scrolls at four rows: a forty-slot bag must not push Confirm off the
   bottom of the screen, and the two offers above it are the part that has to stay in view. */
.rpg-trade-mine { margin-top: var(--sp-3, 10px); padding: var(--sp-3, 10px);
  border-radius: var(--r-2, 7px); border: 1px solid var(--glass-stroke, rgba(255,255,255,.10));
  background: var(--glass-deep, rgba(9,7,26,.78)); }
.rpg-trade-mine-head { display: flex; align-items: baseline; gap: var(--sp-2, 7px);
  margin-bottom: var(--sp-2, 7px); font-family: var(--font-banner); font-size: 11px;
  letter-spacing: .1em; text-transform: uppercase; color: var(--gold); }
.rpg-trade-hint { font: italic 11px var(--font-quill); letter-spacing: 0; text-transform: none;
  color: var(--ink-quill); }
.rpg-trade-bag { grid-template-columns: repeat(10, minmax(0, 1fr)); max-height: 216px;
  overflow-y: auto; min-height: 48px; }
/* Spoken for. Visible, because a grid that reflows as you click it moves the next thing you were
   about to press — and dim, because it is not a thing you can pick up again from here. */
.rpg-trade-bag .rpg-cell.is-spent { opacity: .3; filter: grayscale(1); cursor: default; }
.rpg-trade-bag .rpg-cell.is-spent:hover { border-color: var(--glass-stroke); transform: none; }
.rpg-trade-line { display: flex; align-items: center; gap: var(--sp-2, 7px);
  width: 100%; text-align: left; padding: 5px 8px; border-radius: var(--r-1, 5px);
  border: 1px solid var(--glass-stroke, rgba(255,255,255,.10));
  background: var(--glass, rgba(255,255,255,.03)); color: var(--ink);
  font: 13px var(--font-body); }
button.rpg-trade-line { cursor: pointer; }
button.rpg-trade-line:hover { border-color: var(--ember); color: var(--ember); }
.rpg-trade-item { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis;
  white-space: nowrap; }
.rpg-trade-count { flex: none; color: var(--ink-soft); }
/* Italic quill, the voice used for an aside elsewhere in the interface — an empty half of a trade
   is a description, not a refusal. */
.rpg-trade-empty { margin: 0; font: italic 12px var(--font-quill); color: var(--ink-quill); }
.rpg-trade-coin { display: flex; align-items: center; gap: var(--sp-2, 7px);
  margin-top: var(--sp-3, 10px); font-family: var(--font-banner); font-size: 11px;
  letter-spacing: .1em; text-transform: uppercase; color: var(--gold); }
.rpg-trade-gold { font: 600 14px var(--font-body); color: var(--ink); letter-spacing: 0; }
.rpg-trade-input { width: 92px; padding: 5px 8px; border-radius: var(--r-1, 5px);
  border: 1px solid var(--glass-stroke, rgba(255,255,255,.10));
  background: rgba(0,0,0,.35); color: var(--ink); font: 600 14px var(--font-body); }
.rpg-trade-ready { display: flex; align-items: center; gap: var(--sp-2, 7px);
  margin-top: var(--sp-2, 7px); font-family: var(--font-banner); font-size: 11px;
  letter-spacing: .1em; text-transform: uppercase; color: var(--ink-soft); }
/* THEIR ready mark: the tick is always in the layout and only the colour moves, so the row does
   not jump when it lights — and it is dim-but-legible when unset rather than invisible. */
/* The browser's stock checkbox paints itself system-blue, which is the one colour nowhere in this
   interface — visible only in a screenshot, since a unit test asserting `checked` cannot see it.
   `accent-color` recolours the native control, keeping its keyboard behaviour. */
.rpg-trade-ready input[type=checkbox] { accent-color: var(--sage); width: 15px;
  height: 15px; cursor: pointer; }
.rpg-trade-ready .mark { color: rgba(255,255,255,.16); }
.rpg-trade-ready.is-on { color: var(--sage); }
.rpg-trade-ready.is-on .mark { color: var(--sage); }

.rpg-trade-status { margin-top: var(--sp-3, 10px); font: italic 12px var(--font-quill);
  color: var(--ink-quill); }
.rpg-trade-acts { display: flex; justify-content: flex-end; gap: var(--sp-2, 7px);
  margin-top: var(--sp-3, 10px); }
.rpg-trade-btn { padding: 7px 16px; border-radius: var(--r-2, 7px);
  border: 1px solid var(--glass-stroke, rgba(255,255,255,.10)); background: transparent;
  color: var(--ink-soft); font-family: var(--font-banner); font-size: 11px;
  letter-spacing: .1em; text-transform: uppercase; cursor: pointer; }
.rpg-trade-btn:hover:not(:disabled) { color: var(--ink); border-color: var(--gold); }
.rpg-trade-btn.is-go:not(:disabled) { color: var(--sage); border-color: rgba(146,224,173,.5); }
/* Explicit hex fallback: `--ink-dim` is NOT defined in the design system, and `var(--ink-dim)`
   alone resolves to black, which on this panel renders a disabled control as an empty gap. */
.rpg-trade-btn:disabled { opacity: .85; cursor: not-allowed; color: var(--ink-dim, #8b83a0);
  border-color: rgba(255,255,255,.07); }

/* THE INVITE POPUP. Its own layer above the window, because an invite can arrive while a previous
   session's closing toast is still up, and because it must be reachable with no window at all. */
.rpg-trade-invite { position: fixed; inset: 0; display: none; align-items: center;
  justify-content: center; z-index: 70; pointer-events: none; }
.rpg-trade-invite.is-open { display: flex; }
.rpg-trade-invite-box { pointer-events: auto; padding: var(--sp-4, 14px) var(--sp-5, 18px);
  border-radius: var(--r-3, 10px); border: 1px solid var(--gold);
  background: var(--chrome, rgba(13,10,34,.93)); box-shadow: 0 18px 50px rgba(0,0,0,.55); }
.rpg-trade-invite-text { font: 14px var(--font-body); color: var(--ink);
  margin-bottom: var(--sp-3, 10px); }
.rpg-trade-invite-text strong { color: var(--gold-hi); }
.rpg-trade-invite-acts { display: flex; justify-content: flex-end; gap: var(--sp-2, 7px); }


/* ---- PARTY --------------------------------------------------------------------------------------
   The Party VIEW inside the Social panel, the on-screen party window, and the invite popup.

   NOTE ON `--ink-dim`: it is NOT defined in design-system.css. Used bare it resolves to nothing and
   the declaration is dropped, which on a dark panel renders a disabled control as an EMPTY GAP —
   the trade window carries a fallback for exactly this reason. Nothing below reaches for it; the
   muted ink here is `--ink-mute`, which is real. */

.rpg-party-row { flex-wrap: wrap; }
/* The party rows carry more per row than a friends row (a level, a bar, sometimes a place), so the
   name is allowed to be narrower before it ellipses rather than pushing the bar off the end. */
.rpg-party-row .rpg-social-name { flex: 1 1 120px; }
/* TWO LINES, ALWAYS. Left to wrap on demand these rows were one line when everyone was standing
   with you and two whenever somebody was away — the same list changing shape as people walked
   through doors. `flex-basis: 100%` on the buttons makes every row the same height whatever the
   row contains. */
/* INLINE WITH THE HEALTH BAR (owner's call), not wrapped onto a line of its own. The two actions
   are behind a dot menu now, so the column they need is one button wide — the row no longer has to
   grow a second line to carry them, and the party list is back to being something you glance at. */
.rpg-party-row .rpg-social-acts { flex: none; position: relative; margin-left: 4px; }
.rpg-social-dots { flex: none; width: 24px; height: 24px; padding: 0; line-height: 1;
  border-radius: var(--r-1, 5px); border: 1px solid var(--glass-stroke, rgba(255,255,255,.10));
  background: var(--glass, rgba(255,255,255,.03)); color: var(--ink-soft);
  font-size: 15px; cursor: pointer; transition: border-color .13s, color .13s; }
.rpg-social-dots:hover:not(:disabled) { border-color: var(--gold); color: var(--gold-hi); }
.rpg-social-dots:disabled { opacity: .35; cursor: default; }
/* Anchored to the button's right edge and layered above the rows below it. Right-aligned because
   the button sits at the end of the row: a menu opening rightwards would leave the panel. */
.rpg-social-menu { position: absolute; top: calc(100% + 4px); right: 0; z-index: 8;
  display: flex; flex-direction: column; gap: 4px; min-width: 132px; padding: 6px;
  border-radius: var(--r-2, 7px); border: 1px solid var(--glass-stroke, rgba(255,255,255,.10));
  background: var(--chrome, rgba(13,10,34,.97));
  box-shadow: 0 10px 26px rgba(0,0,0,.55); }
.rpg-social-menu .rpg-social-act { width: 100%; justify-content: center; }

/* THE SEAT COLOUR IS THE PORTRAIT'S RING (owner's call), so a member carries ONE mark rather than a
   strip and a face saying the same thing twice. The width is set here and the hue inline, because
   the hue is per-member data and the geometry is not. */
.rpg-party-face[data-seat] { border-width: 2px; border-style: solid; }
.rpg-party-leader { flex: none; font-family: var(--font-banner); font-size: 10px;
  letter-spacing: .12em; text-transform: uppercase; color: var(--gold-hi);
  border: 1px solid rgba(201,164,110,.45); border-radius: var(--r-pill);
  padding: 1px 7px; }
.rpg-party-lvl { flex: none; font-family: var(--font-mono); font-size: 11px;
  font-variant-numeric: tabular-nums; color: var(--ink-soft); }
/* WHERE THEY ARE, shown only on a row that is not with you. Quill italic — the voice this interface
   uses for an aside — so it reads as a note about the row rather than a second status badge. */
.rpg-party-where { flex: none; font: italic 11px var(--font-quill); color: var(--ink-quill); }

/* THE DIM. An out-of-zone member's bar is a FROZEN reading, and it must not look like a live one.
   Opacity on the whole row rather than a colour swap on the bar: the point is that the entire row
   is stale — the level, the place and the numbers as much as the fill — and dimming one element
   would say the bar is old while implying the rest is current. */
.rpg-party-row.is-away, .rpg-party-win-row.is-away { opacity: .55; }

.rpg-party-hp { display: flex; align-items: center; gap: 6px; flex: 1 1 130px; min-width: 110px; }
.rpg-party-hpbar { position: relative; flex: 1 1 auto; height: 8px; min-width: 46px;
  border-radius: var(--r-pill); overflow: hidden;
  background: rgba(7,9,26,.6); border: 1px solid var(--glass-stroke); }
/* THE SAME RED THE CHARACTER WINDOW USES. It was a hand-picked `#7a1010 -> #e02a22`, a deeper and
   angrier gradient than `--hp-deep -> --hp`, so a party member at full health looked worse than the
   player at full health — two bars for one quantity, painted two ways. The tokens are the whole
   point of having tokens. */
.rpg-party-hpfill { position: absolute; inset: 0; width: 100%; border-radius: var(--r-pill);
  background: linear-gradient(90deg, var(--hp-deep), var(--hp)); transition: width .18s linear; }
/* Low health, the same threshold and the same idea as the hero's own `.rpg-hprow.is-low`. Written
   AFTER the fill rule so source order cannot let the base gradient win it back. */
.rpg-party-hpbar.is-low .rpg-party-hpfill {
  background: linear-gradient(90deg, #b81212, var(--ember)); }

/* ── ONLINE / OFFLINE, ON THE FACE ────────────────────────────────────────────────────────────
   A dot on the portrait's top-left corner. The row already carries the word "Offline" in its
   location line, and a word is something you read; this is something you SEE — scanning five rows
   for who is actually here should not require reading five lines of text. Both, because the dot
   alone is a colour cue and colour alone is not a readout. */
.rpg-party-face { position: relative; }
.rpg-party-dot {
  position: absolute; top: -2px; left: -2px; width: 9px; height: 9px; border-radius: 50%;
  border: 1.5px solid rgba(10,12,22,.9); box-sizing: border-box; pointer-events: none;
}
.rpg-party-dot[data-online="yes"] { background: #57d98a; box-shadow: 0 0 6px rgba(87,217,138,.7); }
.rpg-party-dot[data-online="no"]  { background: #6a6472; box-shadow: none; }

/* THE PLACE RIDES THE NAME LINE, at its right-hand end (owner's call — see the note on
   `.rpg-party-win-top` for why it went to its own row and why that was also wrong).
   `margin-left: auto` pins it to the tail of the line so it starts at a predictable place rather
   than floating wherever the name happens to end. `flex: 0 1 auto` with a `min-width` is the half
   that fixes the original complaint: it SHRINKS under pressure like the name does, but never below
   a few characters, so a long name can no longer push it out of sight entirely. */
.rpg-party-win-row .rpg-party-where {
  display: block; flex: 0 1 auto; min-width: 3.2em; margin-left: auto;
  font-family: var(--font-quill); font-style: italic; font-size: 11px;
  color: var(--ink-soft); opacity: .85;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.rpg-party-hpnum { flex: none; font-family: var(--font-mono); font-size: 11px;
  font-variant-numeric: tabular-nums; color: var(--ink-soft); }

/* THE TWO RULES. Buttons, never radio inputs: a stock radio paints in the browser's system blue,
   which is the one colour nowhere in this interface. */
.rpg-party-rule { margin: var(--sp-3) 0 0; }
/* EVEN-SIZED TOGGLES (owner's call). `flex: 1 1 auto` with the whole rule as the label made each
   option as wide as its own sentence, so a two-way switch came out lopsided and read as two
   unrelated buttons. A grid of equal columns is what a toggle looks like: same width, same weight,
   and the choice is which one is lit rather than which one is bigger.
   No wrap: two options are always one row, and a wrapped toggle is a list. */
.rpg-party-opts { display: grid; grid-auto-flow: column; grid-auto-columns: 1fr;
  gap: var(--sp-2); margin-top: 5px; }
.rpg-party-opt { padding: 8px 10px; border-radius: var(--r-2);
  border: 1px solid var(--glass-stroke); background: transparent; color: var(--ink-soft);
  font: 12px var(--font-body); text-align: center; cursor: pointer;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.rpg-party-opt:hover:not(:disabled) { color: var(--ink); border-color: var(--gold); }
.rpg-party-opt.is-on { color: var(--gold-hi); border-color: rgba(201,164,110,.55);
  background: rgba(255,240,200,.07); }
/* A DEAD CONTROL MUST STILL BE READABLE. `.85` opacity and a real muted ink, so a frozen leader
   control reads as "not now" rather than vanishing into the panel. */
.rpg-party-opt:disabled, .rpg-social-act:disabled { opacity: .62; cursor: not-allowed;
  color: var(--ink-mute); border-color: rgba(255,255,255,.07); }
.rpg-social-act:disabled:hover { color: var(--ink-mute); border-color: rgba(255,255,255,.07); }
/* THE SELECTED RULE KEEPS ITS GOLD WHILE FROZEN. Written after the `:disabled` block, because that
   block won on source order and stripped the highlight off `.is-on` — a frozen panel that no longer
   said WHICH rule the party was playing under. Caught by rendering the offline-leader state; the
   markup was correct in both, and only the paint had lost the answer. */
.rpg-party-opt.is-on:disabled { color: var(--gold-hi); border-color: rgba(201,164,110,.45);
  background: rgba(255,240,200,.05); }

/* THE ON-SCREEN PARTY WINDOW, under the character info box.
   `.rpg-status` is 306px wide at top:14px/left:14px; this sits below it in the same column, so the
   two read as one stack of information about who you are and who you are with. */
.rpg-party-window { position: absolute; left: 14px; width: 236px; display: none;
  border: 2px solid transparent; border-radius: var(--r-2);
  background:
    linear-gradient(var(--chrome), var(--chrome)) padding-box,
    var(--frame-trim-soft) border-box;
  box-shadow: var(--frame-inner-glow), 0 6px 18px -6px rgba(0,0,0,.55);
  padding: var(--sp-2) var(--sp-3); z-index: 12;
  /* NOT pointer-events:none, unlike `.rpg-status`: this one has a control on it. */
  pointer-events: auto; }
/* BELOW THE STATUS BLOCK. `top` is a hand-measured offset rather than a flow position because the
   status block is absolutely positioned furniture and nothing lays the two out together. The quest
   tracker sits under the minimap on the other side, so nothing collides here. */
.rpg-party-window { top: 188px; }
.rpg-party-window.is-open { display: block; }
.rpg-party-win-head { display: flex; align-items: center; justify-content: space-between;
  gap: var(--sp-2); }
.rpg-party-win-title { font-family: var(--font-banner); font-size: 11px; letter-spacing: .12em;
  text-transform: uppercase; color: var(--gold); }
.rpg-party-win-toggle { flex: none; width: 20px; height: 20px; line-height: 1;
  border: 1px solid var(--glass-stroke); border-radius: var(--r-1, 5px); background: transparent;
  color: var(--ink-soft); cursor: pointer; font-size: 11px; }
.rpg-party-win-toggle:hover { color: var(--ink); border-color: var(--gold); }
/* MINIMISED is the header alone. The body is emptied by the renderer as well, so this is belt and
   braces rather than the only mechanism — a folded window must not keep laying out rows. */
.rpg-party-window.is-min .rpg-party-win-body { display: none; }
/* SCROLLS AT FULL STRENGTH. Ten members is the cap and it is the layout that decides this
   window's size: unbounded, a full party runs off the bottom of the screen. Capped in vh (not px)
   so a short viewport gets a scroller rather than a window that overlaps the hotbar. */
.rpg-party-win-body { display: flex; flex-direction: column; gap: 5px; margin-top: 6px;
  max-height: 52vh; overflow-y: auto;
  /* EXPLICIT, because `overflow-y: auto` alone computes the x axis to `auto` as well — the
     horizontal scrollbar that bug produced was never asked for by any rule. This window is a
     fixed-width column of rows; there is nothing to the side of it to scroll to, so a row that
     ever does exceed the width should be clipped, not made reachable. */
  overflow-x: hidden; }
/* A FLEX-BASIS IN A COLUMN IS A HEIGHT. `.rpg-party-hp` carries `flex: 1 1 130px` for the panel's
   rows, which are laid out in a ROW — inside these column rows the same declaration made every
   member 130px TALL, and a full party of ten ran to about 1700px. Rendered and caught; the numbers
   were all correct and only the axis was wrong. */
.rpg-party-win-row .rpg-party-hp { flex: 0 0 auto; width: 100%; }
.rpg-party-win-row { display: flex; flex-direction: column; gap: 3px;
  padding: 5px 7px; border-radius: var(--r-2);
  border: 1px solid var(--glass-stroke); background: var(--glass-deep); }
/* THE LEADER'S ROW IS MARKED BY THE CROWN ALONE (owner's call). It used to carry a gold left edge
   as well, from before the crown existed — and once every portrait grew a coloured ring, a gold bar
   down one row's edge was a third colour on a row that already had two, saying something the crown
   beside the name had already said. */
/* ONE LINE: name, crown, level, place (owner's call). A member cost two text rows plus a bar, and
   ten of those is most of the left-hand side of the screen.
   THE TRAP THIS HAS FALLEN INTO TWICE, from both directions. The location first lived on this line
   and was reported INVISIBLE — it was `flex: none` against a name that could shrink, so the name
   took the room and the place was pushed out past the clip. It was then given `width: 100%` to
   claim a row of its own, which a `nowrap` flex line simply ADDED to the other three: 192px inside
   a 159px box, which is where the party window's horizontal scrollbar came from.
   Neither fix was right because both picked a WINNER. The answer is that the two variable-length
   items — the player's name and the place they are standing — BOTH shrink and BOTH ellipse, so
   whichever is long gives way and neither disappears. The crown and the level are fixed and never
   shrink; they are short, and they are the two a player reads at a glance. */
.rpg-party-win-top { display: flex; flex-wrap: nowrap; align-items: baseline; gap: 6px;
  min-width: 0; }
/* THE NAME HUGS ITS TEXT, so the crown sits against it (owner's call). At `flex: 1 1 auto` the
   name expanded to fill the line and pushed the crown all the way right, where it ended up against
   the LEVEL and read as decoration on the number rather than as a mark on the person. `0 1 auto`
   still shrinks and still ellipses on a long name — it just does not grow past it. The place at the
   end of the line takes the slack instead (`margin-left: auto`). */
.rpg-party-win-name { flex: 0 1 auto; min-width: 2.5em; overflow: hidden; text-overflow: ellipsis;
  white-space: nowrap; font: 600 12px var(--font-body); color: var(--ink); }

/* THE INVITE POPUP. Its own layer, because an invite arrives when there is no party window at all.
   Deliberately the same geometry and the same button class as the trade invite: two popups that ask
   the same shape of question should not be two different objects to learn. */
.rpg-party-invite { position: fixed; inset: 0; display: none; align-items: center;
  justify-content: center; z-index: 71; pointer-events: none; }
.rpg-party-invite.is-open { display: flex; }
.rpg-party-invite-box { pointer-events: auto; padding: var(--sp-4, 14px) var(--sp-5, 18px);
  border-radius: var(--r-3, 10px); border: 1px solid var(--gold);
  background: var(--chrome, rgba(13,10,34,.93)); box-shadow: 0 18px 50px rgba(0,0,0,.55); }
.rpg-party-invite-text { font: 14px var(--font-body); color: var(--ink);
  margin-bottom: var(--sp-3, 10px); }
.rpg-party-invite-text strong { color: var(--gold-hi); }
.rpg-party-invite-acts { display: flex; justify-content: flex-end; gap: var(--sp-2, 7px); }

/* ---- BESTIARY -----------------------------------------------------------------------------------
   A grid of every creature in the game. Wider than Social because it is a collection you scan
   rather than a list you act on. */
.rpg-panel--bestiary { width: min(760px, 94vw); }
/* Inline at the end of the controls row now, not a paragraph of its own — see the markup note. */
.rpg-bestiary-summary { font: 13px var(--font-quill); color: var(--ink-quill);
  /* BESIDE THE TITLE, not shoved to the far edge. `margin-left: auto` put the tally hard against
     the close button with the whole title bar of empty parchment between them, so it read as a
     separate control rather than as a subtitle of the word it counts. */
  margin: 0; flex: none; white-space: nowrap; }
/* EXACTLY FOUR COLUMNS, not as many as fit. `auto-fill` gave a different creature count per
   viewport width, so "the fourth tile" meant a different animal on every machine and the 4x4 page
   the owner asked for existed only at one window size. */
.rpg-bestiary-grid { display: grid; gap: var(--sp-3);
  grid-template-columns: repeat(4, minmax(0, 1fr)); }
.rpg-bestiary-tile { display: flex; flex-direction: column; align-items: center; gap: 5px;
  padding: var(--sp-3); border-radius: var(--r-2);
  border: 1px solid var(--glass-stroke); background: var(--glass-deep); text-align: center; }
/* THE PORTRAIT PLATE. A ROUNDED PANEL, not the circle this shipped as: the art is a creature
   rendered side-on, and most of them (snake, sauropod, wasp) are far wider than they are tall, so a
   circular plate had them hanging out of both sides of it. */
.rpg-bestiary-art { width: 76px; height: 60px; display: grid; place-items: center;
  border-radius: var(--r-2); overflow: hidden;
  font-family: var(--font-display); font-size: 20px; letter-spacing: .02em; }
.rpg-bestiary-tile.is-known .rpg-bestiary-art { color: var(--gold-hi);
  background: radial-gradient(circle at 50% 34%, var(--plum-hi), var(--plum-lo));
  border: 1px solid var(--gold-deep); }
/* THE SILHOUETTE'S GROUND HAS TO BE LIGHTER THAN THE SILHOUETTE. This plate was near-black (#0b0920)
   while it held a dark letter; a BLACK creature on it would have been an invisible shape in an empty
   box. Lifted to a muted slate so the outline reads, and the dashed rim kept — it is what says
   "unfilled" rather than "broken". */
.rpg-bestiary-tile.is-unknown .rpg-bestiary-art { color: #2a2444; background: #4a4468;
  border: 1px dashed rgba(255,255,255,.10); }
.rpg-bestiary-img { width: 100%; height: 100%; object-fit: contain; display: block;
  -webkit-user-drag: none; user-select: none; }
/* THE SILHOUETTE ITSELF — ONE LINE, and the reason there is no second set of PNGs.
   `brightness(0)` multiplies every colour channel by zero and does not touch alpha, so the creature
   comes out solid black in exactly its own outline. It is the SAME FILE as the revealed tile, so the
   silhouette can never be of a different animal, a different pose, or a stale render. */
.rpg-bestiary-img.is-silhouette { filter: brightness(0); opacity: .88; }
/* The fallback mark, for a creature whose portrait has not been rendered. */
.rpg-bestiary-mark { font-family: var(--font-display); font-size: 20px; }
.rpg-bestiary-tile.is-unknown { opacity: .72; }
.rpg-bestiary-name { font: 600 12px var(--font-body); color: var(--ink); }
.rpg-bestiary-tile.is-unknown .rpg-bestiary-name { color: var(--ink-mute); font-style: italic; }
/* 12px, up a fifth from the 10 it shipped at (owner's call). The "not yet encountered" state
   shares this class and grows with it, which is deliberate: the two states occupy the same row of
   every tile, and letting them differ in height would make the grid jog as creatures are found. */
.rpg-bestiary-count { font-family: var(--font-mono); font-size: 12px;
  font-variant-numeric: tabular-nums; color: var(--ink-soft); }
.rpg-bestiary-count.is-unknown { font-family: var(--font-quill); font-style: italic;
  color: var(--ink-mute); }

