/* ============================================================
   The app shell — the chrome around a mode.

   Two rules from packages/ui/styles/tokens.css run through all of it:

     SQUARE IS THE RECORD, ROUNDED IS THE ACTION. A universe card is
     square — it is a thing you have. The mode tabs, the pancake and
     every button are rounded — they do something.

     COLOUR IS DATA. The chrome is achromatic. Brass appears on exactly
     one thing here, the tab you are on, because that is the same
     meaning it carries everywhere else: where you are in an order.

   THE HEADER SITS ABOVE BATTLEMODE'S OWN TOPBAR RATHER THAN REPLACING
   IT. `app.js` is not being rewritten yet (see ARCHITECTURE.md), so it
   keeps the DOM it has always had and this bar is kept thin enough to
   stack on it. An earlier version of this file put the mode switcher at
   the BOTTOM for exactly that reason; tabs belong at the top with the
   universe name, so the compromise moved rather than disappeared. When
   event becomes a real mode its toolbar should come up into this
   header's right-hand side and the second bar goes away.

   Selectors are scoped to the shell's own class names. Event
   injects its stylesheet after this one and also defines `.btn`, so
   anything shared is qualified by an ancestor rather than left to
   source order.
   ============================================================ */

:root {
  --head-h: 48px;
  /* The stage starts under the header, and the header is only up on a
     mode screen — a master list deliberately has no mode tabs. */
  --stage-top: 0px;

  /* THE DETAIL PANE'S WIDTH, AND THERE ARE TWO OF THEM — because the
     pane does two different jobs and they are not the same size.

       detail  THE DEFAULT, AND A FOOTNOTE. Rules text beside the thing
               it explains: a grant on the character sheet, a species
               trait, a condition. The SHEET is the page here, and a
               footnote that takes half the screen has stopped being a
               footnote — what it is annotating has to stay legible
               beside it. Wide enough for a paragraph at a comfortable
               measure and no wider. It scales with the window because
               giving 420px away is nothing on a 27" display and is
               most of a laptop.

       half    A LIST AND THE RECORD IT OPENED, and here the pane IS
               the page. Items, creatures — the list is an index down
               the side and everything a reader came for is in the
               pane: two columns of facts, a portrait, a thread of
               moments. Squeezing that into 420px while a single
               column of names keeps two thirds of the screen is
               exactly backwards, so the room is split evenly.

     WHOEVER OPENS IT SAYS WHICH (`size` on `pane:open`), because only
     the surface that built the content knows whether it is a footnote
     or the point. Default is `detail`: a caller that says nothing gets
     what every caller got before this existed.

     AND EITHER CAN BE DRAGGED. The grip writes `--pane-w` inline on
     `.stage`, which beats both rules below, and pane.js remembers it
     PER SIZE — a reader who likes a wide item pane has said nothing
     about how wide they want a footnote. */
  --pane-w: clamp(300px, 30vw, 420px);
  --pane-min: 260px;

  /* IS THE PANE COVERING THE PAGE? 1 or 0, flipped by the one media
     query at the bottom of this file. It is here so that pane.js can
     ASK rather than carry its own copy of `880px` — a breakpoint
     written in two places is a breakpoint that will disagree with
     itself, and what depends on this one is not styling: it is whether
     Back closes the pane and whether the page underneath can still be
     tabbed into. */
  --pane-cover: 0;

  /* HOW NARROW THE CONTENT AREA MAY BE MADE. Below this the pane stops
     taking room from the page and overlays instead — see the note at
     `.detail-pane`. This is the number that stops a two-column sheet
     being squeezed into one column of single words. */
  --content-min: 560px;
}
html[data-view='play'] { --stage-top: calc(var(--head-h) + 48px); }
/* The status bar is up on the builder pages and takes its height off
   the bottom of the stage rather than covering it. */
html[data-view='list'] { --status-h: 26px; }

/* Build keeps its record strip. Play has one moment and view switches. */
.section-switch {
  border: 1px solid var(--rule); border-radius: 6px;
  padding: 7px 12px; background: transparent; color: var(--ink);
  font: inherit; cursor: pointer; white-space: nowrap;
}
.section-switch:hover, .section-switch:focus-visible {
  border-color: var(--accent-brass); outline: 2px solid var(--accent-brass); outline-offset: 2px;
}
.section-current { color: var(--ink); font-weight: 600; margin-inline: 6px; }
.play-context {
  position: fixed; top: var(--head-h); inset-inline: 0; height: 48px;
  display: flex; align-items: center; gap: 12px; padding: 0 16px;
  background: var(--card); border-bottom: 1px solid var(--rule); z-index: 39;
  color: var(--ink); font: 13px system-ui, sans-serif;
}
.play-context[hidden] { display: none; }
.play-context strong { max-width: 25%; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
.play-context label { color: var(--ink-dim); }
.play-context select {
  min-width: 100px; max-width: 30%; background: var(--board);
  color: var(--ink); border: 1px solid var(--rule); padding: 6px;
}
#playNotice { color: var(--ink-dim); overflow: hidden; }
@media (max-width: 640px) {
  .play-context { gap: 6px; padding-inline: 8px; }
  .play-context label, .section-current { display: none; }
  .play-context strong { max-width: 28%; }
  .section-switch { padding: 6px 8px; }
}

/* A LIST NEEDS FAR LESS ROOM THAN A SHEET, and that is what pays for
   the even split. 560px is the width a two-column character sheet
   stops working below; a column of names and a search box is still
   itself at 380. Without this second number the 50/50 would give way
   to the sheet's minimum on any laptop and never actually be half. */
.stage[data-pane-size='half'] { --pane-w: 50%; --content-min: 380px; }

/* ------------------------------------------------------------
   Buttons — ROUNDED IS THE ACTION.

   SCOPED, and that is the point. These lived in gate.css as a bare
   `.btn` and leaked straight into event, whose own buttons are
   11px pills with 5px of padding: an unscoped `min-height: 44px`
   inflated every control in the tracker. Event injects its
   stylesheet AFTER this one, so source order does not save us — only
   an ancestor does.

   44px is the iOS minimum and is right for a shell control someone
   taps once. It is wrong for a dense table toolbar, and the shell has
   no business deciding that for a mode.
   ------------------------------------------------------------ */

:is(.app-header, .menu-panel, .universe-page, .sb-head, .host-room-panel) .btn {
  min-height: 44px; padding: 0 18px; border-radius: 10px;
  border: 1px solid var(--rule); background: var(--card); color: var(--ink);
  font-family: var(--caps); text-transform: uppercase; letter-spacing: .06em;
  font-size: 14px; cursor: pointer;
}
:is(.app-header, .menu-panel, .universe-page, .sb-head, .host-room-panel) .btn.primary {
  background: var(--accent-brass); color: #17130a; border-color: var(--accent-brass);
}
:is(.app-header, .menu-panel, .universe-page, .sb-head, .host-room-panel) .btn.quiet {
  background: transparent; color: var(--ink-dim); border-color: transparent;
}
:is(.app-header, .menu-panel, .universe-page, .sb-head, .host-room-panel) .btn.big {
  min-height: 50px; font-size: 14px;
}
:is(.app-header, .menu-panel, .universe-page, .sb-head, .host-room-panel) .btn[disabled] {
  opacity: .5; cursor: default;
}

/* ------------------------------------------------------------
   Which screen is up

   Four views, and only one of them shows a mode. The universe page
   and universe settings are full screens because choosing a universe
   is a departure from what you were doing — and because everything
   below is keyed under the answer.

   EVERY ONE OF THEM HAS AN ADDRESS. `data-view` says what is painted;
   the hash says the same thing where the browser can see it, so Back
   works and a screen can be pointed at (packages/shell/src/route.js).
   ------------------------------------------------------------ */

/* Every mode but event paints into the mount. Event keeps the
   DOM it has always had, hidden rather than removed. */
html[data-playview]:not([data-playview='event']) .topbar,
html[data-playview]:not([data-playview='event']) .workspace,
html[data-playview]:not([data-playview='event']) .legend { display: none; }

html:is([data-view='universes'], [data-view='settings'], [data-view='list'], [data-view='playing']) :is(
  .topbar, .workspace, .legend, .mode-mount
) { display: none; }

/* ------------------------------------------------------------
   THE STAGE — the content area and the detail pane, side by side

   TWO SIBLING BOXES IN ONE FLEX ROW, and that is the whole design.
   Opening the pane hands the content area a NARROWER BOX, so whatever
   is on screen lays itself out again in the room it has. The version
   before this one was a `position: fixed` panel plus `padding-right`
   on the page inside it, and it produced precisely the layout it was
   meant to avoid: the page went on centring itself in the FULL width
   while 400px was taken out of the middle, so the side margins stayed
   exactly where they were and the columns collapsed to one word per
   line. Room has to come off the OUTSIDE of a page, which means the
   page has to be told how wide it is — not padded from within.

   `container-type` makes this box a size container, so a page inside
   it can respond to the space it actually has rather than to the size
   of the window. That is what lets the character sheet reflow when the
   pane opens and not when it does not. It also makes the stage the
   containing block for `position: fixed` descendants, which is why a
   mode's modal covers the content area and leaves the header and the
   pane alone — correct for a pane that is not modal, and worth knowing
   before adding a third one.
   ------------------------------------------------------------ */

.stage {
  position: fixed; inset: var(--stage-top) 0 var(--status-h, 0px) 0;
  display: flex; align-items: stretch; overflow: hidden;
  background: var(--board);
}
.stage[hidden] { display: none; }

.stage-main {
  position: relative; flex: 1 1 auto;
  min-width: var(--content-min);
  container-type: inline-size;
  overflow: hidden;
}

/* Event's markup is in FLOW here rather than being an absolute
   screen, because app.js is not being rewritten yet and keeps the DOM
   it has always had: a topbar, a workspace that takes what is left,
   and a legend. Its own stylesheet made `body` the flex column that
   did that; the column is here now, and the 14px gutter with it.
   `.legend` and the popups app.js appends to `<body>` are the two
   things still outside — the popups position themselves against
   `window.innerWidth`, so with the pane open they can land under it.
   That is event's rewrite to fix, not the shell's. */
html[data-playview='event'][data-view='play'] .stage-main {
  display: flex; flex-direction: column; padding: 0 14px 8px;
}

.mode-mount {
  position: absolute; inset: 0;
  overflow: auto; background: var(--board);
}

/* ---- the pane ----
   Its frame only: width, header, a scrolling body. What is in it
   belongs to whichever surface opened it. */
.detail-pane {
  /* THE CONTENT AREA'S MINIMUM IS ENFORCED HERE, not by letting flex
     fight it out. `min-width` on `.stage-main` and a non-shrinking
     pane over-constrain the row, and what flexbox does with an
     over-constrained row is overflow it — the pane goes off the right
     edge and its close button with it. Said as a `min()` on the basis
     instead, the pane simply takes what is left over the content's
     floor, and the number that decides it is the same `--content-min`
     the rest of this file talks about. A percentage basis resolves
     against `.stage`, which is the flex container: half of the stage
     is half of what the reader can see. */
  flex: 0 0 min(var(--pane-w), calc(100% - var(--content-min)));
  position: relative; min-width: 0; z-index: 40;
  display: grid; grid-template-rows: auto minmax(0, 1fr);
  background: var(--card); border-left: 1px solid var(--rule);
  overflow: hidden;
}
.detail-pane[hidden] { display: none; }

/* ---- the grip ----
   THE SPLIT IS A DEFAULT, NOT A RULING. Half is right for most item
   records and wrong for the one someone is reading three paragraphs
   of, and the shell has no way to know which. So the edge is draggable
   and the number is remembered per size (pane.js).

   ACHROMATIC, LIKE THE REST OF THE CHROME. Colour is data here and a
   window divider is not data — it thickens and lightens under the
   pointer instead. It sits ON the pane's own border rather than
   between the two boxes, so it takes no room in the row and moving it
   moves nothing but itself.

   9px WIDE AND 1px VISIBLE. The hairline is the border that was
   already there; the rest is target. A 1px grab handle is a handle
   only for people who never miss. */
.pane-grip {
  position: absolute; top: 0; bottom: 0; left: 0; width: 9px; z-index: 41;
  appearance: none; border: 0; padding: 0; background: transparent;
  cursor: col-resize; touch-action: none;
}
.pane-grip::before {
  content: ''; position: absolute; top: 0; bottom: 0; left: 0; width: 1px;
  background: var(--rule);
  transition: width .12s ease, background-color .12s ease;
}
.pane-grip:is(:hover, :focus-visible)::before,
.pane-grip[data-drag='true']::before { width: 3px; background: var(--ink-dim); }
/* THE BAR IS THE FOCUS RING. A default outline on a 9px strip draws a
   rectangle down the whole seam and reads as a rendering fault, so the
   grip says it has focus the way it says it is being dragged — by
   getting brighter than a hover does. */
.pane-grip:focus-visible { outline: none; }
.pane-grip:focus-visible::before { width: 3px; background: var(--ink); }

/* THE WHOLE WINDOW IS THE DRAG SURFACE ONCE IT HAS STARTED. Without
   this the cursor flickers back to a text caret over every label the
   pointer crosses, and a fast drag selects the page behind it. */
html[data-pane-drag='true'] { cursor: col-resize; user-select: none; }
html[data-pane-drag='true'] .detail-pane > *:not(.pane-grip) { transition: none; }

/* THE LAYOUT SNAPS AND THE CONTENT ARRIVES. Animating the width would
   re-lay the whole page out on every frame of a 220ms transition —
   a sheet with two scrolling columns cannot afford that, and the
   reflow is visible as a stutter in the thing you are trying to read.
   So the room is given in one reflow and only the pane's own content
   travels. */
.detail-pane > * {
  transition: opacity .18s ease, transform .18s cubic-bezier(.2, .7, .3, 1);
}
.detail-pane[data-enter='true'] > *:not(.pane-grip) { opacity: 0; transform: translateX(10px); }
@media (prefers-reduced-motion: reduce) {
  .detail-pane > * { transition: none; }
}
/* The grip is the pane's EDGE, not its contents. Fading it in with the
   text would mean the one control that says "this is draggable" is
   invisible for the first fifth of a second every time. */
.detail-pane > .pane-grip { transition: none; }

.detail-head {
  display: flex; gap: 12px; align-items: start; justify-content: space-between;
  padding: 14px 16px; border-bottom: 1px solid var(--rule);
}
.detail-title { display: grid; gap: 3px; min-width: 0; }
.detail-title em {
  font-family: var(--caps); font-style: normal; text-transform: uppercase;
  letter-spacing: .1em; font-size: 11px; color: var(--ink-dim);
}
.detail-title em[hidden] { display: none; }
.detail-title strong {
  font-family: var(--serif); font-weight: 400; font-size: 18px; line-height: 1.25;
  color: var(--ink);
}
/* Rounded: it does something. */
.detail-x {
  flex: 0 0 auto; appearance: none; cursor: pointer;
  min-height: 30px; min-width: 30px; border-radius: 999px;
  border: 1px solid transparent; background: transparent;
  color: var(--ink-dim); font-size: 18px; line-height: 1;
}
.detail-x:hover { border-color: var(--rule); color: var(--ink); }
/* ONE BUTTON, TWO GLYPHS, AND CSS PICKS. It does the same thing at
   both widths — it closes the pane — so it stays one control with one
   label; what changes is which word for it the layout is telling the
   truth with. Beside the page, dismissing a footnote is a cross. Over
   the page, leaving the screen is an arrow. */
.detail-x-back { display: none; }

.detail-body { min-height: 0; overflow-y: auto; padding: 14px 16px 32px; }

/* ============================================================
   WHERE THERE IS NO ROOM TO SIT BESIDE, THE PANE IS THE SCREEN

   `--content-min` plus the pane's own minimum is about 880px. Under
   that there is no split to make: the two boxes are each other's
   margins, and a 340px sliver of item record beside a 340px sliver of
   list is two things too narrow to read instead of one that is.

   AN EARLIER VERSION SPLIT THE DIFFERENCE — the pane came out of the
   flex row and floated over 92vw of the right-hand side. That is the
   worst of both: it covers the page anyway, and it leaves a 30px strip
   of it showing down the left as if something behind were still in
   play. Nothing behind it is in play. So it takes the whole STAGE —
   everything the reader can see bar the app header, which is the
   chrome the stage hangs under and not part of the page it covers.

   AND THAT CHANGES WHAT IT IS, NOT JUST HOW WIDE IT IS. A pane beside
   the page is a footnote on that page — state, written into the query
   with replace(), and Back steps over it because nobody wants six
   presses to leave a sheet they looked six things up on. A pane that
   COVERS the page is a page. It gets a history entry, Back closes it,
   and the control in its corner is an arrow rather than a cross —
   because "close" and "go back" are the same gesture once the thing
   being closed is everything you can see. chrome.js is where that
   decision is written down; this file only says when it applies.
   ============================================================ */
@media (max-width: 880px) {
  .stage { --pane-cover: 1; }
  .stage-main { min-width: 0; }
  .detail-pane {
    position: absolute; inset: 0;
    border-left: 0;
    /* Still on the leading edge while it travels, and harmless once it
       has arrived — there is nothing beside it to cast onto. */
    box-shadow: -18px 0 40px rgba(0, 0, 0, .45);
    transition: transform .22s cubic-bezier(.2, .7, .3, 1);
  }
  .detail-pane[data-enter='true'] { transform: translateX(100%); }
  .detail-pane[data-enter='true'] > * { opacity: 1; transform: none; }
  @media (prefers-reduced-motion: reduce) {
    .detail-pane { transition: none; }
  }

  /* NOTHING TO DRAG. The pane is the screen and the screen is one
     width, so an edge that invites a drag is an edge that lies. */
  .pane-grip { display: none; }

  /* THE ARROW GOES WHERE ARROWS GO. Leading edge, ahead of the title,
     because at this width it is the only way out of the pane AND the
     only Back control on screen — the list's own is underneath it. */
  .detail-head { flex-direction: row-reverse; justify-content: flex-end; }
  .detail-x-close { display: none; }
  .detail-x-back { display: inline; }
  .detail-x { font-size: 17px; }
}

/* ------------------------------------------------------------
   The header
   ------------------------------------------------------------ */

.app-header {
  position: fixed; top: 0; left: 0; right: 0; height: var(--head-h);
  display: flex; align-items: center; gap: var(--gap);
  padding: 0 var(--gap);
  background: var(--card); border-bottom: 1px solid var(--rule);
  z-index: 50;
}
.app-header[hidden] { display: none; }
.app-header-spacer { flex: 1; }

/* Rounded: pressing it takes you to the universe page. The brass rule
   on the left is the same mark event puts on its own wordmark —
   this is the name of the thing you are inside. */
.universe-name {
  appearance: none; border: 0; background: transparent; cursor: pointer;
  display: flex; flex-direction: column; align-items: flex-start; gap: 1px;
  padding: 4px 10px; border-radius: 8px; min-width: 0; max-width: 30vw;
  border-left: 3px solid var(--accent-brass);
}
.universe-name:hover { background: rgba(255,255,255,.05); }
.universe-label {
  font-family: var(--caps); text-transform: uppercase; letter-spacing: .18em;
  font-size: 11px; color: var(--ink-dim); line-height: 1;
}
.universe-value {
  font-family: var(--serif); font-size: 14px; color: var(--ink); line-height: 1.15;
  max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

/* ---- mode tabs ---- */
.mode-tabs {
  display: flex; align-items: stretch; gap: 2px;
  overflow-x: auto; scrollbar-width: none; min-width: 0;
}
.mode-tabs::-webkit-scrollbar { display: none; }
.mode-tabs button {
  appearance: none; border: 0; background: transparent; color: var(--ink-dim);
  font-family: var(--caps); text-transform: uppercase; letter-spacing: .09em;
  font-size: 11px; padding: 0 14px; white-space: nowrap;
  cursor: pointer; border-radius: 8px; margin: 7px 0;
}
.mode-tabs button:hover:not(:disabled) { color: var(--ink); background: rgba(255,255,255,.05); }
.mode-tabs button[aria-current='true'] {
  color: var(--accent-brass); background: color-mix(in srgb, var(--accent-brass) 14%, transparent);
}
.mode-tabs button:disabled { opacity: .4; cursor: not-allowed; text-decoration: line-through; }

/* The only place the network is ever visible to a DM. */
.app-header .pip {
  width: 8px; height: 8px; border-radius: 50%; flex: 0 0 auto;
  background: var(--pip, var(--ink-dim));
}

/* Development only — the marker for a locally minted entitlement.
   Steel, because it arrived from somewhere other than the real signer. */
.app-header .dev-chip {
  appearance: none; cursor: pointer; flex: 0 0 auto;
  border: 1px solid color-mix(in srgb, var(--steel) 45%, var(--rule));
  color: var(--steel); background: color-mix(in srgb, var(--steel) 12%, transparent);
  border-radius: 5px; padding: 3px 7px; font-size: 11px;
  font-family: var(--caps); text-transform: uppercase; letter-spacing: .08em;
}

@media (max-width: 640px) {
  .app-header { gap: 6px; padding-inline: 8px; }
  .app-header .universe-name { max-width: 72px; padding-inline: 4px; }
  .app-header .mode-tabs { flex: 1; }
  .app-header .mode-tabs button { padding-inline: 8px; min-height: 32px; }
  .app-header .dev-chip, .app-header .app-header-spacer { display: none; }
  .app-header .pancake { flex-shrink: 0; }
}

/* ---- the pancake ---- */
.pancake {
  appearance: none; background: transparent; border: 1px solid transparent;
  border-radius: 8px; cursor: pointer; flex: 0 0 auto;
  width: 38px; height: 38px; display: grid; align-content: center; gap: 4px;
  padding: 0 9px;
}
.pancake:hover { background: rgba(255,255,255,.05); }
.pancake span { display: block; height: 1.5px; background: var(--ink-dim); border-radius: 2px; }
.pancake[aria-expanded='true'] span { background: var(--ink); }

.menu-scrim { position: fixed; inset: 0; z-index: 55; background: rgba(0,0,0,.5); }
.menu-scrim[hidden] { display: none; }

.menu-panel {
  position: fixed; top: calc(var(--head-h) + 6px); right: var(--gap);
  width: min(300px, calc(100vw - var(--gap) * 2));
  background: var(--card); border: 1px solid var(--rule); border-radius: 12px;
  z-index: 60; padding: 6px; box-shadow: 0 14px 40px rgba(0,0,0,.5);
}
.menu-panel[hidden] { display: none; }
.menu-section { padding: var(--sp-3) var(--sp-1); }
.menu-section + .menu-section { border-top: 1px solid var(--rule); }
.menu-panel .menu-heading { margin: 0 0 var(--sp-1); }
.menu-account {
  margin: 0 0 var(--sp-1); padding: 0 10px; font: 400 var(--fs-body)/1.4 var(--sans); color: var(--ink);
  overflow: hidden; text-overflow: ellipsis;
}
:is(#stage, .menu-panel) .menu-panel .menu-item, .menu-panel .menu-item { width: 100%; }
.menu-panel .menu-item[hidden] { display: none; }
.menu-facts {
  margin: 10px 0 0; display: grid; grid-template-columns: auto 1fr;
  gap: 4px 10px; font-family: var(--sans); font-size: 11px;
}
.menu-facts dt { color: var(--ink-dim); }
.menu-facts dd { margin: 0; color: var(--ink); overflow-wrap: anywhere; }

/* ------------------------------------------------------------
   The universe page, and universe settings
   ------------------------------------------------------------ */

.universe-page {
  position: absolute; inset: 0; z-index: 30;
  overflow-y: auto; background: var(--board);
}
.universe-page[hidden] { display: none; }
/* THE SIDE PADDING IS THE FIRST THING TO GIVE WAY. `cqi` is a percent
   of the content area, not of the window, so the gutter narrows when
   the detail pane takes room and the text does not. The auto margins
   go to nothing before that — which is the order a reader wants:
   whitespace first, content last. */
.universe-page-inner {
  max-width: 860px; margin: 0 auto;
  padding: 40px clamp(12px, 2.4cqi, 24px) 64px;
}
.universe-page-head { margin-bottom: var(--sp-5); }   /* the page head part (parts.css §16) draws the rest */
.universe-page-head .back { min-height: 36px; font-size: 11px; }
/* The way out and the way sideways, on one line above the title. */
.page-head-row {
  display: flex; align-items: center; justify-content: space-between;
  gap: 14px; margin-bottom: 14px;
}

.universe-list {
  display: grid; gap: 14px;
  grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
  margin-bottom: 36px;
}

/* SQUARE — a universe is a record you have, not a control. */
.universe-card {
  appearance: none; text-align: left; cursor: pointer;
  display: flex; flex-direction: column; gap: 6px;
  padding: 0 0 16px; overflow: hidden;
  background: var(--card); border: 1px solid var(--rule); border-radius: 0;
  color: var(--ink);
}
.universe-card:hover { border-color: color-mix(in srgb, var(--accent-brass) 40%, var(--rule)); }
.universe-card[data-current='true'] { border-color: var(--accent-brass); }
.universe-card > * { padding-left: 16px; padding-right: 16px; }

.universe-banner {
  display: block; width: 100%; height: 96px; object-fit: cover;
  padding: 0; margin-bottom: 4px; border-bottom: 1px solid var(--rule);
}
.universe-card h2 {
  margin: 14px 0 0; font-family: var(--serif); font-weight: 400; font-size: 18px;
}
.universe-card .universe-banner + h2 { margin-top: 10px; }
.universe-system {
  margin: 0; font-family: var(--caps); text-transform: uppercase;
  letter-spacing: .1em; font-size: 11px; color: var(--ink-dim);
}
.universe-summary {
  margin: 4px 0 0; font-family: var(--sans); font-size: 13px; line-height: 1.5;
  color: var(--ink-dim);
}
.universe-open {
  align-self: flex-start; margin-top: 8px;
  font-family: var(--caps); text-transform: uppercase; letter-spacing: .1em;
  font-size: 11px; color: var(--accent-brass);
  border: 1px solid color-mix(in srgb, var(--accent-brass) 45%, var(--rule));
  border-radius: 4px; padding: 2px 6px;
}

/* A refusal nobody can see is indistinguishable from a crash. */
.universe-card.unreadable { cursor: default; border-color: var(--oxblood); }
.universe-id { margin: 0; font-family: var(--mono); font-size: 11px; color: var(--ink-dim); }
.universe-warn {
  margin: 6px 0 0; font-family: var(--sans); font-size: 13px; line-height: 1.5;
  color: var(--ink); border-left: 2px solid var(--oxblood); padding-left: 8px;
}

.universe-new { display: grid; gap: 8px; max-width: 460px; }
.universe-new-row { display: flex; gap: 8px; }
.universe-new-row .gate-field { flex: 1; }
.universe-import { display: grid; gap: 6px; max-width: 460px; margin-top: 10px; }
.universe-import-btn { justify-self: start; cursor: pointer; }
#importBundleNote { white-space: pre-line; }
.import-agree {
  display: grid; gap: 8px; padding: 12px 14px; border: 1px solid var(--rule);
  border-left: 2px solid var(--oxblood); background: var(--card);
  font-family: var(--sans); font-size: 13px; line-height: 1.5; color: var(--ink);
}
.import-agree[hidden] { display: none; }
.import-agree p { margin: 0; }
.import-agree-head { font-weight: 600; }
.import-agree-by { color: var(--ink-dim); }
.import-agree-points { margin: 0; padding-left: 20px; display: grid; gap: 4px; }
.import-agree-check { display: flex; gap: 8px; align-items: flex-start; font-weight: 600; cursor: pointer; }
.import-agree-check input { margin-top: 3px; }
.import-agree-actions { display: flex; gap: 8px; justify-content: flex-end; }
.universe-new-note {
  margin: 0; min-height: 1.2em; font-family: var(--sans); font-size: 13px;
  color: var(--ink-dim);
}

/* ---- master lists ----
   A list gets the whole screen, and NOT the mode tabs: nothing about
   "where the table is" applies while you are looking at everything that
   exists. The header is hidden for the same reason. */
/* ---- the Story Builder header ----

   TWO ROWS WITH TWO JOBS. The top says where you are and never moves;
   the tab strip under it is the working set. `.list-head` — one row
   with the list's name and summary in it — is gone: under tabs the
   name of what you are looking at is on the TAB, and a header that
   also said it would be saying it twice. */
.sb-head {
  position: sticky; top: 0; z-index: 2;
  display: flex; flex-direction: column;
  background: var(--card); border-bottom: 1px solid var(--rule);
}
.sb-top {
  display: flex; align-items: center; gap: 18px;
  padding: 9px clamp(12px, 2.4cqi, 24px); min-height: 52px;
}
.sb-top :is(.back, .up) { min-height: 32px; font-size: 11px; flex: 0 0 auto; }
.sb-spacer { flex: 1 1 auto; }

.sb-universe { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
.sb-universe .k {
  font-family: var(--caps); text-transform: uppercase;
  letter-spacing: .15em; font-size: 11px; color: var(--ink-dim);
}
.sb-universe .n {
  font-family: var(--serif); font-size: 14px; color: var(--ink);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* WHICH SECTION THIS IS, and it is a label rather than a title —
   the rule to its left separates it from the universe without
   spending a second line on it. */
.sb-section {
  flex: 0 0 auto;
  font-family: var(--caps); text-transform: uppercase;
  letter-spacing: .17em; font-size: 11px; color: var(--ink);
  padding-left: 18px; border-left: 1px solid var(--rule);
}
@media (max-width: 620px) { .sb-section { display: none; } }

.sb-pip {
  flex: 0 0 auto; width: 7px; height: 7px; border-radius: 50%;
  background: var(--pip, var(--ink-dim));
}

/* ---- the tab strip ----

   ROUNDED, WITH EDGES AND A CLOSE, because a builder tab is an OBJECT
   the reader opened and owns — and because Play's views are a flat
   underline. The two are drawn differently on purpose: they look
   alike and behave oppositely, and the shapes are what stop anybody
   merging them. Rounded is also the product's own rule for anything
   pressable. */
.sb-tabs {
  display: flex; align-items: flex-end; gap: 3px;
  padding: 0 clamp(8px, 1.6cqi, 16px);
  overflow-x: auto; overflow-y: hidden;
  scrollbar-width: thin;
}
.sb-tabs:empty { display: none; }

.sb-tab {
  flex: 0 0 auto;
  display: flex; align-items: center; gap: 7px;
  padding: 8px 8px 8px 11px;
  margin-bottom: -1px;
  background: transparent;
  border: 1px solid transparent; border-bottom: 0;
  border-radius: 7px 7px 0 0;
  color: var(--ink-dim);
  font-family: var(--sans); font-size: 13px; line-height: 1.3;
  white-space: nowrap; max-width: 220px;
  cursor: pointer;
}
.sb-tab:hover { color: var(--ink); }
/* THE ONE IN FRONT IS THE ONE THE BOARD SHOWS THROUGH. Painting it the
   content's own background is what makes a tab read as attached to the
   page under it rather than as a pressed button. */
.sb-tab[aria-selected='true'] {
  background: var(--board); border-color: var(--rule); color: var(--ink);
}
.sb-tab-label { overflow: hidden; text-overflow: ellipsis; min-width: 0; }
.sb-tab-icon { flex: 0 0 auto; width: 15px; height: 15px; display: block; color: var(--ink-dim); }
.sb-tab[aria-selected='true'] .sb-tab-icon { color: var(--accent-brass); }

/* The close is a button inside a tab, and a button inside a button is
   not a thing a browser will honour — so the tab is a `div` with a
   role and both presses are its children. */
.sb-tab-x {
  flex: 0 0 auto;
  display: flex; align-items: center; justify-content: center;
  width: 18px; height: 18px; padding: 0;
  border: 0; border-radius: 50%; background: transparent;
  color: var(--ink-dim); font-size: 14px; line-height: 1;
  opacity: .5; cursor: pointer;
}
.sb-tab:hover .sb-tab-x, .sb-tab[aria-selected='true'] .sb-tab-x { opacity: .9; }
.sb-tab-x:hover { background: var(--rule); color: var(--ink); opacity: 1; }

.sb-tab-new {
  flex: 0 0 auto; margin: 0 0 6px 6px; padding: 2px 11px;
  border: 1px solid var(--rule); border-radius: 999px;
  background: transparent; color: var(--ink-dim);
  font-family: var(--sans); font-size: 14px; line-height: 1.4;
  cursor: pointer;
}
.sb-tab-new:hover { color: var(--ink); border-color: color-mix(in srgb, var(--accent-brass) 40%, var(--rule)); }

/* NOTHING OPEN IS A REAL STATE — a universe somebody has just made has
   no tabs — and it gets a sentence rather than a blank. */
.sb-blank {
  margin: 0; padding: 48px 24px; text-align: center;
  color: var(--ink-dim); font-size: 14px;
}
/* A MASTER LIST IS AN APP, NOT A DOCUMENT. #listScreen holds a head and a
   mount and nothing else, so the head can stop scrolling away and the
   mount can own the scrolling instead. That gives a mode a KNOWN
   HEIGHT to lay itself out against — without one, a pane that wants to
   be "the rest of the screen" has no rest of the screen to be, and a
   sticky column just spills off the bottom. Scoped to the list screen
   by id: .universe-page is also the universe list, which is a document
   and should go on scrolling like one. */
#listScreen { display: flex; flex-direction: column; overflow: hidden; }
/* AN ID BEATS `.universe-page[hidden]`, so this has to be said again
   here or the list screen is laid out while it is hidden — an empty
   `← Back` bar over whatever mode is actually up. */
#listScreen[hidden] { display: none; }
#listScreen .sb-head { position: static; flex: 0 0 auto; }
/* THE MOUNT IS THE POSITIONING CONTEXT, and that is not a detail.
   A surface that wants to BE the screen — story's `.sm` is one, and
   every board inside it is another — says `position: absolute; inset: 0`
   and expects to fill the box it was handed. Without a relative mount
   the nearest positioned ancestor is `#listScreen`, which includes the
   header and the tab strip, so the surface starts at the top of the
   SCREEN instead of the top of the MOUNT: story's toolbar and its
   workspace tabs landed behind the shell's own header and simply could
   not be seen. One line here, rather than one in every mode. */
/* ONE PER LIST, and no box of its own — see `hostFor` in chrome.js.
   It exists so that a list surface holds a node the shell can take out
   of the document, which is the only way "this screen is not on the
   glass" can be asked of a view that is kept alive between visits. */
.sb-list-host { display: contents; }
.list-mount { display: block; position: relative; }
#listScreen .list-mount { flex: 1 1 auto; min-height: 0; overflow-y: auto; }

.menu-lists { display: grid; gap: 0; }
:is(#stage, .menu-panel) .menu-item.list-item { flex-direction: column; align-items: flex-start; justify-content: center; gap: 2px; padding-block: var(--sp-1); }
.menu-panel .list-item strong { font: 600 var(--fs-body)/1.3 var(--sans); color: var(--ink); }
.list-item-summary { font: 400 var(--fs-small)/1.35 var(--sans); color: var(--ink-dim); white-space: normal; }
.menu-panel .list-item:disabled { opacity: .45; }

/* ---- settings ---- */
.settings-form { display: grid; gap: 10px; max-width: 560px; }
.settings-form .gate-field.area {
  font-family: var(--sans); font-size: 14px; line-height: 1.55; resize: vertical;
}
.settings-banner { display: grid; gap: 10px; margin-bottom: 10px; }
.banner-preview {
  height: 150px; border: 1px solid var(--rule); background: #0d0b09;
  display: grid; place-items: center; overflow: hidden;
}
.banner-preview img { width: 100%; height: 100%; object-fit: cover; }
.banner-preview[data-empty='true'] span {
  font-family: var(--caps); text-transform: uppercase; letter-spacing: .12em;
  font-size: 11px; color: var(--ink-dim);
}
.banner-actions { display: flex; gap: 8px; }
/* A <label> styled as a button — it opens the file picker, so it is an
   action and gets the rounded treatment along with the rest. */
.banner-actions .btn {
  display: inline-flex; align-items: center; font-size: 11px; min-height: 38px;
}

.theme-list {
  display: grid; gap: 10px; margin: 4px 0 10px;
  grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
}
/* Rounded: choosing a theme is an action, and it is the one control on
   this page that is not a field. */
.theme-card {
  appearance: none; cursor: pointer; text-align: left;
  display: grid; gap: 6px; padding: 12px;
  background: var(--card); border: 1px solid var(--rule); border-radius: 10px;
  color: var(--ink);
}
.theme-card:hover { border-color: color-mix(in srgb, var(--accent-brass) 40%, var(--rule)); }
.theme-card[aria-checked='true'] { border-color: var(--accent-brass); }
.theme-card strong { font-family: var(--serif); font-weight: 400; font-size: 16px; }
.theme-summary { font-family: var(--sans); font-size: 11px; line-height: 1.45; color: var(--ink-dim); }

/* A personality model has no colour to swatch, so what it shows
   instead is its shape — how many questions it asks. */
.theme-shape {
  font-family: var(--caps); text-transform: uppercase; letter-spacing: .1em;
  font-size: 11px; color: var(--ink-dim); opacity: .75;
}
.settings-hint {
  margin: 0 0 10px;
  font-family: var(--sans); font-size: 13px; line-height: 1.5; color: var(--ink-dim);
}

/* The five colours that carry meaning, in the order the product rule
   lists them. A theme is chosen by looking at it, and what is worth
   looking at is what happens to these — not to the chrome. */
.theme-swatch { display: flex; gap: 4px; }
.theme-swatch i {
  display: block; width: 16px; height: 16px; border-radius: 0;
  border: 1px solid var(--rule); background: var(--rule);
}

/* ------------------------------------------------------------
   Rulesets

   A ruleset ROW is a record, so it is square. Every control on it —
   pick, disclose, download — is an action, so those are rounded. That
   is the same rule the rest of the product is drawn by, and it is
   doing real work here: the row and the buttons inside it would
   otherwise be four nested rectangles with nothing saying which one
   the pointer is for.
   ------------------------------------------------------------ */
.ruleset-list { display: grid; gap: 8px; margin-bottom: 12px; }

.ruleset-row {
  display: grid; grid-template-columns: 1fr auto; align-items: start; gap: 10px;
  padding: 10px 12px;
  background: var(--card); border: 1px solid var(--rule); border-radius: 2px;
}
.ruleset-row[data-chosen='true'] { border-color: var(--accent-brass); }

.ruleset-pick {
  appearance: none; cursor: pointer; text-align: left;
  display: grid; gap: 5px; padding: 4px 6px;
  background: none; border: 1px solid transparent; border-radius: 8px;
  color: var(--ink);
}
.ruleset-pick:hover { border-color: color-mix(in srgb, var(--accent-brass) 40%, var(--rule)); }
.ruleset-pick strong { font-family: var(--serif); font-weight: 400; font-size: 16px; }

/* The chosen ruleset is marked on the ROW, so the tick has to be
   drawn by something that knows it — the label, not the border. */
.ruleset-row[data-chosen='true'] .ruleset-pick strong::after {
  content: ' ✓'; color: var(--accent-brass);
}

.ruleset-files > summary {
  cursor: pointer; list-style: none;
  padding: 5px 12px; border: 1px solid var(--rule); border-radius: 999px;
  font-family: var(--caps); text-transform: uppercase; letter-spacing: .08em;
  font-size: 11px; color: var(--ink-dim); white-space: nowrap;
}
.ruleset-files > summary::-webkit-details-marker { display: none; }
.ruleset-files > summary:hover { border-color: var(--accent-brass); color: var(--ink); }
.ruleset-files[open] > summary { border-color: var(--accent-brass); color: var(--ink); }

.ruleset-file-list { display: grid; gap: 4px; margin-top: 8px; min-width: 190px; }

/* One file, one button: the name of the part and what it weighs.
   The weight is the whole reason the split exists, so it is not a
   detail hidden behind a hover. */
.ruleset-file {
  appearance: none; cursor: pointer;
  display: flex; justify-content: space-between; gap: 14px; align-items: baseline;
  padding: 5px 10px;
  background: none; border: 1px solid var(--rule); border-radius: 8px;
  color: var(--ink); font-family: var(--sans); font-size: 11px;
}
.ruleset-file:hover { border-color: var(--accent-brass); }
.ruleset-file.all { margin-top: 4px; border-style: dashed; }
.ruleset-part { text-transform: capitalize; }
.ruleset-size { color: var(--ink-dim); font-variant-numeric: tabular-nums; }

.ruleset-import { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; margin-bottom: 4px; }
.settings-note.warn { color: var(--oxblood); }

.settings-actions { display: flex; align-items: center; gap: 12px; margin-top: 6px; }
.settings-note { font-family: var(--sans); font-size: 13px; color: var(--ink-dim); }

.mode-empty:not(.empty-note) {
  margin: 0; padding: calc(var(--gap) * 6) calc(var(--gap) * 3);
  text-align: center; color: var(--ink-dim); font-family: var(--sans); font-size: 14px;
}

/* The kicker above a master list's name. Small, and it earns its space:
   "Items" alone does not say whether you are looking at everything in
   the world or at what the party has on them right now, and that is
   the exact distinction the Modes / Master Lists split exists to make.
   Saying it on the screen costs one line and removes the question. */
.list-kicker {
  font-size: 11px; letter-spacing: .09em; text-transform: uppercase;
  color: var(--ink-dim);
}

/* ------------------------------------------------------------
   Content libraries

   A CHECKBOX ROW, NOT A CARD, and the difference is the point: a
   universe wears ONE theme and ONE ruleset, so those are radios that
   look like choices. It may have any number of libraries, and their
   ORDER is the conflict rule — so this reads as a list you can
   reorder rather than a set you pick from.
   ------------------------------------------------------------ */
.library-list { display: grid; gap: 6px; margin-bottom: 12px; }

.library-row {
  display: grid; grid-template-columns: 1fr auto; align-items: center; gap: 10px;
  padding: 8px 12px;
  border: 1px solid var(--rule); border-radius: 2px; background: var(--card);
}
.library-row[data-on='true'] { border-color: var(--accent-brass); }

.library-toggle {
  display: flex; align-items: start; gap: 10px; cursor: pointer; min-width: 0;
}
.library-toggle input { margin-top: 3px; flex: none; accent-color: var(--accent-brass); }
.library-body { display: grid; gap: 3px; min-width: 0; }
.library-body strong { font-family: var(--serif); font-weight: 400; font-size: 14px; }

.library-actions { display: flex; align-items: center; gap: 6px; }
/* Rounded, because they are actions. */
.btn.tiny {
  padding: 3px 9px; font-size: 11px; border-radius: 999px; line-height: 1.6;
}

#playerRequests { position: relative; }
#playerRequestList { max-height: 55vh; overflow: auto; padding: 12px; min-width: 260px; }
.player-request { padding: 12px 0; border-bottom: 1px solid var(--rule); }
.player-request input { width: 100%; margin-bottom: 8px; }
.player-request .btn { margin-right: 8px; }

#playerRequestList { position: absolute; top: 100%; right: 0; width: min(360px, 85vw); background: var(--card, #211d17); border: 1px solid var(--rule); z-index: 50; }

#hostRoom { position: relative; color: var(--ink); font: 13px var(--sans); }
.host-room-panel { position: absolute; right: 0; top: 100%; width: min(360px, 85vw); max-height: 75vh; display: flex; flex-direction: column; overflow: hidden; padding: 16px; z-index: 50; background: var(--card, #211d17); border: 1px solid var(--rule); }
.host-room-panel label { display: block; margin: 12px 0; }
.host-room-panel input { width: 100%; min-height: 44px; }

/* ------------------------------------------------------------
   THE STATUS BAR — page on the left, saving on the right.

   Saving is ONE indicator, not a sentence per system: a dot and a
   short phrase for this device, then the backup in a word or two. The
   whole story is in its tooltip, including that the two are separate.
   ------------------------------------------------------------ */
.status-bar {
  position: fixed; left: 0; right: 0; bottom: 0; height: var(--status-h, 26px);
  display: none; align-items: center; gap: 16px; padding: 0 14px;
  border-top: 1px solid var(--rule); background: var(--card);
  font-size: 11px; color: var(--ink-dim); z-index: 20;
}
html[data-view='list'] .status-bar { display: flex; }
.status-page { flex: 1; min-width: 0; display: flex; gap: 14px; overflow: hidden; white-space: nowrap; }
.status-page > span { overflow: hidden; text-overflow: ellipsis; }
.status-page > span:first-child { color: var(--ink); }
.status-save { flex: none; display: flex; align-items: center; gap: 7px; white-space: nowrap; }
.status-save[hidden], .status-word[hidden] { display: none; }
/* A slot as wide as its widest word: every word in one cell, one shown. */
.status-word { display: inline-grid; }
.status-word > span { grid-area: 1 / 1; visibility: hidden; }
.status-word > span.on { visibility: visible; }
.status-dot { box-sizing: border-box; width: 7px; height: 7px; border-radius: 50%; background: var(--ink-dim); }
.status-save[data-state='saved'] .status-dot { background: var(--moss); }
.status-save[data-state='pending'] .status-dot { background: var(--brass); }
/* Saving: the same seven pixels, as a turning ring. */
.status-save[data-state='saving'] .status-dot {
  background: none; border: 1.5px solid var(--brass); border-right-color: transparent;
  animation: status-spin .8s linear infinite;
}
@keyframes status-spin { to { transform: rotate(1turn); } }
@keyframes status-pulse { 50% { opacity: .35; } }
@media (prefers-reduced-motion: reduce) {
  .status-save[data-state='saving'] .status-dot {
    border-right-color: var(--brass); animation: status-pulse 1.6s ease-in-out infinite;
  }
}
.status-save[data-state='error'] .status-local { color: var(--ink); }
.status-save[data-state='error'] .status-dot { background: var(--oxblood); }
.status-backup { justify-items: end; padding-right: 7px; border-right: 1px solid var(--rule); }
.status-backup[data-state='conflict'], .status-backup[data-state='auth-required'] { color: var(--ink); }

.host-room-panel textarea { width: 100%; resize: vertical; min-height: 70px; }
.host-room-actions { display: flex; flex-wrap: wrap; gap: 8px; }
.host-participant { padding: 12px 0; border-top: 1px solid var(--rule); }
.host-participant p { margin: 6px 0 10px; }
.host-participant .btn + .btn { margin-left: 8px; }

.host-room-panel, .host-room-panel * { box-sizing: border-box; }
.host-room-panel { line-height: 1.5; }
.host-room-panel input, .host-room-panel textarea {
  background: var(--board); color: var(--ink); border: 1px solid var(--rule);
  border-radius: 0; padding: 10px; font: inherit;
}
.host-room-panel input:focus-visible, .host-room-panel textarea:focus-visible {
  outline: 2px solid var(--brass); outline-offset: 2px;
}
.host-room-panel .muted { color: var(--ink-dim); }
.host-room-panel label { color: var(--ink-dim); }
.host-room-panel summary { cursor: pointer; padding-block: 8px; }
.host-room-panel h3 { margin: 16px 0 8px; font-size: 14px; }

.host-room-body { min-height: 0; overflow-y: auto; overscroll-behavior: contain; padding: 0 2px 12px; }
.host-room-actions { flex-shrink: 0; padding-top: 12px; border-top: 1px solid var(--rule); }
.host-room-confirm { flex-shrink: 0; max-height: 35vh; overflow-y: auto; }
.host-room-confirm[hidden] { display: none; }

/* ---- The player's side (playing.js) ---- */
.playing-page { color: var(--ink); }
.playing-page :is(label, legend, p, summary) { color: inherit; }
.playing-page .pancake span { background: var(--ink); }
.playing-head-row { margin-bottom: var(--sp-2); }
.playing-rows { list-style: none; margin: 0; padding: 0; border: 1px solid var(--rule); background: var(--card); }
.playing-row { display: flex; align-items: center; gap: var(--sp-3); flex-wrap: wrap; padding: var(--sp-3) var(--sp-4); min-height: 56px; }
.playing-row + .playing-row { border-top: 1px solid var(--rule); }
.playing-row > strong { flex: 1 1 12rem; font: 600 19px/1.2 var(--display, var(--serif)); color: var(--ink); }
.playing-row > .field { width: auto; flex: 0 1 14rem; }
/* Make a character: a name, the ruleset to start under, and the press. */
.playing-make { display: flex; flex-wrap: wrap; gap: var(--sp-2); align-items: center; margin: 0 0 var(--sp-4); }
.playing-make > .field { width: auto; flex: 1 1 12rem; }
.playing-make > select.field { flex: 0 1 14rem; }
.playing-who { color: var(--ink); }
.playing-row > strong > a { color: inherit; text-decoration: none; }
.playing-row > strong > a:hover { text-decoration: underline; }
/* A party's page: where to write, then what has been passed round. */
.playing-party { display: grid; gap: var(--sp-4); max-width: 760px; }
.playing-compose { display: grid; gap: var(--sp-2); padding: var(--sp-4); border: 1px solid var(--rule); background: var(--card); }
.playing-send { display: flex; align-items: center; gap: var(--sp-3); flex-wrap: wrap; }
.playing-send > .playing-status { margin: 0; }
.playing-compose > select.field { justify-self: start; width: auto; min-width: 14rem; }
.playing-posts { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--sp-3); }
.playing-post { padding: var(--sp-3) var(--sp-4); border: 1px solid var(--rule); background: var(--card); }
/* Among the players: the one kind the GM never sees, marked so. */
.playing-post[data-audience='players'] { border-left: 3px solid var(--brass); }
.playing-post[data-audience='one'] { border-left: 3px dotted var(--ink-dim); }
.playing-post.is-editing { display: grid; gap: var(--sp-2); }
.playing-post-head { display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--sp-2); }
.playing-post-head > strong { font: 600 17px/1.25 var(--display, var(--serif)); color: var(--ink); }
.playing-post-head > button { margin-left: auto; }
.playing-post-doc { margin-top: var(--sp-2); color: var(--ink); }
.playing-post-picture { display: block; max-width: 100%; max-height: 420px; margin-top: var(--sp-2); border: 1px solid var(--rule); }
.playing-post-body { margin: var(--sp-2) 0 0; white-space: pre-wrap; color: var(--ink); }
.playing-rules { display: inline-flex; align-items: center; gap: var(--sp-2); flex-wrap: wrap; }
.playing-signin { display: grid; gap: var(--sp-2); max-width: 420px; }
.playing-signin > .field { width: 100%; }
.playing-signin > .btn { justify-self: start; }
.playing-status { min-height: 1.4em; color: var(--ink-dim); }
.playing-signin [hidden], .playing-nav .count[hidden] { display: none !important; }
