/*
 * A finished-looking MUD client (P10.1), built on the StormFront-lite frame
 * P6.2 shipped and mud-engine's own dr-ui.md / mushclient-notes.md research.
 * No build step and no CDN: everything, fonts included, is served from this
 * directory under the server's own restrictive self-only CSP.
 *
 * D2 (D2_DESIGN_BUILD.md, UI_DIRECTION.md "Direction A"): VELLUM — the game read
 * as a chronicle. Iron-gall ink on vellum, a rubric red for what matters, gilt
 * for labels and frame hairlines, lapis for links and focus, verdigris for exits
 * and gains. The log is set in a book serif (Alegreya); tables, the prompt and
 * the command echo stay monospace (IBM Plex Mono). Panes are folios: a ruled
 * edge and an inner hairline, not a drop shadow.
 *
 * Theme: dark ("night vellum") is the default; the light palette ("day vellum")
 * applies under `prefers-color-scheme: light`, and `[data-theme]` (Settings)
 * wins over the OS in either direction. Every colour is a token on `:root`; the
 * light blocks only override tokens. tools/client/contrast.mjs (and
 * dist/server/clientD2.test.js) read these blocks and hold every text pair at
 * 4.5:1 and every control edge at 3:1, in both themes.
 *
 * MOTION RULES (D2, G-MOTION):
 *  - Nothing moves unless the visitor has NOT asked for reduced motion
 *    (`@media (prefers-reduced-motion: no-preference)`) AND Settings' Effects is
 *    not "off" (`body[data-effects="off"]` stops every animation and transition
 *    below, whatever rule started it).
 *  - Animate `transform` and `opacity` only (a bar's width transition is the
 *    one historical exception, P10.6).
 *  - No information is carried by motion alone: every pulse, flash or fade has a
 *    static twin (an outline, a colour, a word) that stays when motion is off.
 *  - Durations come from the tokens: --dur-quick (UI state), --dur-calm (art
 *    and sheets), --dur-slow (the backdrop cross-fade).
 */
@font-face { font-family: "Alegreya"; font-style: normal; font-weight: 400 900; font-display: swap; src: url("/fonts/alegreya.woff2") format("woff2"); }
@font-face { font-family: "Alegreya"; font-style: italic; font-weight: 400; font-display: swap; src: url("/fonts/alegreya-italic.woff2") format("woff2"); }
@font-face { font-family: "Alegreya SC"; font-style: normal; font-weight: 500; font-display: swap; src: url("/fonts/alegreya-sc-500.woff2") format("woff2"); }
@font-face { font-family: "Alegreya SC"; font-style: normal; font-weight: 700; font-display: swap; src: url("/fonts/alegreya-sc-700.woff2") format("woff2"); }
@font-face { font-family: "IBM Plex Mono"; font-style: normal; font-weight: 400; font-display: swap; src: url("/fonts/plex-mono.woff2") format("woff2"); }

:root {
  /* ---- night vellum (the default) ---- */
  --bg: #16110c;
  --frame: #16110c;
  --panel: #211a12;      /* a folio's surface: the log, the panes, the dock */
  --well: #0f0b07;       /* sunk: the command line, inputs, the map ground */
  --frame-line: #3d3224; /* the inner hairline */
  --rule-strong: #85714f;/* a control's edge (3:1 on --panel) */
  --ink: #eadfc8;
  --ink-dim: #ad9c7f;
  --ink-bright: #faf3e3;
  --rubric: #e8806a;     /* THE accent: room titles, danger, the current step, the primary action */
  --gold: #ddb256;       /* gilt: labels, the level, a frame's hairline (never running text) */
  --gold-dim: #b4924a;
  --gilt-line: #b38d3c;  /* a frame's gilt hairline (decoration) */
  --lapis: #94b4ea;      /* links, tells, the Training lines */
  --verd: #86c6a6;       /* exits, gains, done steps */
  --elite: #c3a6f0;
  --focus: #94b4ea;
  --on-accent: #1a120b;  /* text on a --rubric fill */

  /* The vitals bar's own track and label ink, deliberately NOT swapped by
     the light block: a label sits on a saturated fill, not the page, so the
     track stays dark and the label near-white in both themes (P10.x). */
  --bar-well: #0b0805;
  --bar-ink: #fff8ea;

  --hp: #a3321f;
  --mana: #2b5596;
  --moves: #2e6a4c;
  --xp: #5e4588;
  --hurt: #e8806a;
  --gain: #86c6a6;
  --death: #ddb256;

  --s-tell: #dda3d3;
  --s-gossip: #8fc9d2;
  --s-shout: #e9a86a;
  --s-talk: #b6cf94;
  --s-group: #94b4ea;
  --s-system: #ad9c7f;
  --s-deaths: #e8806a;

  /* P14.1: atmospheric visuals — set by app.js's `applyAtmosphere`
     (view.js), read by `body::after`'s day/night overlay below. Defaults
     to "no effect at all" so a client that never receives a `Char.Vitals`
     renders exactly as before this row. */
  --atmo-tint: rgba(0, 0, 0, 0);
  --atmo-vignette: 0;

  --exit: #86c6a6;

  /* P10.3: bottom bar */
  --group: #2d6b66;
  --tick: #ddb256;
  --chip-bg: #1a140e;
  --chip-ink: #eadfc8;
  --chip-posture: #94b4ea;
  --chip-combat: #e8806a;
  --chip-link: #e9a86a;
  --chip-afk: #ad9c7f;
  --chip-grouped: #94b4ea;
  --chip-wimpy: #e9a86a;
  --chip-effect: #86c6a6;
  --chip-stance: #ad9c7f;
  --compass-dark: #5a4b37;

  /* D2: rarity (text colour + a static glow; the glow is decoration, the colour and the word carry it) */
  --rarity-glow-rare: rgba(148, 180, 234, 0.45);
  --rarity-glow-epic: rgba(221, 163, 211, 0.5);
  --rarity-glow-legendary: rgba(221, 178, 86, 0.6);

  /* D2: the backdrop behind the log (the room's painting, blurred) and the log's own opacity over it */
  --backdrop-filter: blur(22px) brightness(0.32) saturate(1.05);
  --backdrop-room-filter: blur(14px) brightness(0.36) saturate(1.05);
  --log-opacity: 90%;
  --shadow-dialog: 0 18px 48px -20px rgba(0, 0, 0, 0.7);
  /* PTV-FIXES §3: how strongly the hour grade (body::after) and the blurred backdrop show. Dark keeps them as D2/V2
     drew them; light (below) keeps the parchment warm and clean, as UI_DIRECTION.md's Vellum mockup draws it. */
  --atmo-strength: 1;
  --atmo-vignette-rgb: 0, 0, 0;
  --backdrop-veil: 0;

  /* ---- type ---- */
  --f-book: "Alegreya", "Iowan Old Style", Palatino, "Palatino Linotype", "Book Antiqua", Georgia, serif;
  --f-caps: "Alegreya SC", "Alegreya", Palatino, "Palatino Linotype", Georgia, serif;
  --mono: "IBM Plex Mono", ui-monospace, "Cascadia Mono", Consolas, "Liberation Mono", "DejaVu Sans Mono", monospace;
  /* `--sans` is the UI face every older rule names: in Vellum that is the book face too. */
  --sans: var(--f-book);
  /* The scale (a major second on the root): */
  --t-xs: 0.8125rem;
  --t-sm: 0.875rem;
  --t-base: 1rem;
  --t-log: 1.0625rem;
  --t-md: 1.25rem;
  --t-lg: 1.3rem;

  /* ---- space, shape, motion ---- */
  --gap: 0.375rem;
  --space-1: 4px;
  --space-2: 6px;
  --space-3: 10px;
  --space-4: 14px;
  --space-5: 22px;
  --radius: 2px;
  --dur-quick: 160ms;
  --dur-calm: 400ms;
  --dur-slow: 900ms;

  /* P13.1: the "UI scale" setting (Settings dialog, 80%-150%, default
     "auto") — a plain multiplier (unitless number, never a length: it is
     multiplied into `html`'s own font-size below via `calc()`). app.js's
     `applyUiScale` sets this on `documentElement` from `settings.uiScale`;
     every rem value in this file is relative to THAT computed font-size, so
     one slider scales text, spacing, panel widths and portraits together. */
  --ui-scale: 1;
  color-scheme: dark;
}

/* P10.4: an explicit theme override (Settings, "system/dark/light") wins
   over the OS preference in either direction. The light palette is restated
   under `:root[data-theme="light"]` so it applies even under a dark OS; the
   two light blocks must stay identical (tools/client/contrast.mjs checks). */
@media (prefers-color-scheme: light) {
  :root:not([data-theme="dark"]) {
    --bg: #ede3cc;
    --frame: #e6dac0;
    --panel: #f7f1e3;
    --well: #ede3cc;
    --frame-line: #c4b28c;
    --rule-strong: #8f7b57;
    --ink: #2b2118;
    --ink-dim: #66543f;
    --ink-bright: #150f09;
    --rubric: #a0301f;
    --gold: #735208;
    --gold-dim: #675535;
    --gilt-line: #b8912f;
    --lapis: #1e4a8a;
    --verd: #2b6247;
    --elite: #5b3d84;
    --focus: #1e4a8a;
    --on-accent: #fffbf2;
    --bar-well: #3a2e22;
    --bar-ink: #fffbf2;
    --hp: #9e2b1f;
    --mana: #1e4a8a;
    --moves: #2b6247;
    --xp: #5b3d84;
    --hurt: #a0301f;
    --gain: #2b6247;
    --death: #735208;
    --s-tell: #8a2f7e;
    --s-gossip: #1d5e6a;
    --s-shout: #8a460c;
    --s-talk: #3f5f1f;
    --s-group: #1e4a8a;
    --s-system: #66543f;
    --s-deaths: #a0301f;
    --exit: #2b6247;
    --group: #1f5f5a;
    --tick: #735208;
    --chip-bg: #f2ead6;
    --chip-ink: #2b2118;
    --chip-posture: #1e4a8a;
    --chip-combat: #a0301f;
    --chip-link: #8a460c;
    --chip-afk: #66543f;
    --chip-grouped: #1e4a8a;
    --chip-wimpy: #8a460c;
    --chip-effect: #2b6247;
    --chip-stance: #66543f;
    --compass-dark: #b3a07c;
    --rarity-glow-rare: rgba(30, 74, 138, 0.18);
    --rarity-glow-epic: rgba(138, 47, 126, 0.18);
    --rarity-glow-legendary: rgba(184, 145, 47, 0.35);
    --backdrop-filter: blur(22px) brightness(1.1) saturate(0.55) sepia(0.35);
    --backdrop-room-filter: blur(14px) brightness(1.1) saturate(0.6) sepia(0.3);
    --shadow-dialog: 0 18px 48px -20px rgba(58, 40, 18, 0.45);
    --log-opacity: 100%;
    --atmo-strength: 0.4;
    --atmo-vignette-rgb: 92, 62, 28;
    --backdrop-veil: 0.72;
    color-scheme: light;
  }
}

/* Restated so an explicit "light" override applies even under a dark OS (see the comment above the light media query). */
:root[data-theme="light"] {
    --bg: #ede3cc;
    --frame: #e6dac0;
    --panel: #f7f1e3;
    --well: #ede3cc;
    --frame-line: #c4b28c;
    --rule-strong: #8f7b57;
    --ink: #2b2118;
    --ink-dim: #66543f;
    --ink-bright: #150f09;
    --rubric: #a0301f;
    --gold: #735208;
    --gold-dim: #675535;
    --gilt-line: #b8912f;
    --lapis: #1e4a8a;
    --verd: #2b6247;
    --elite: #5b3d84;
    --focus: #1e4a8a;
    --on-accent: #fffbf2;
    --bar-well: #3a2e22;
    --bar-ink: #fffbf2;
    --hp: #9e2b1f;
    --mana: #1e4a8a;
    --moves: #2b6247;
    --xp: #5b3d84;
    --hurt: #a0301f;
    --gain: #2b6247;
    --death: #735208;
    --s-tell: #8a2f7e;
    --s-gossip: #1d5e6a;
    --s-shout: #8a460c;
    --s-talk: #3f5f1f;
    --s-group: #1e4a8a;
    --s-system: #66543f;
    --s-deaths: #a0301f;
    --exit: #2b6247;
    --group: #1f5f5a;
    --tick: #735208;
    --chip-bg: #f2ead6;
    --chip-ink: #2b2118;
    --chip-posture: #1e4a8a;
    --chip-combat: #a0301f;
    --chip-link: #8a460c;
    --chip-afk: #66543f;
    --chip-grouped: #1e4a8a;
    --chip-wimpy: #8a460c;
    --chip-effect: #2b6247;
    --chip-stance: #66543f;
    --compass-dark: #b3a07c;
    --rarity-glow-rare: rgba(30, 74, 138, 0.18);
    --rarity-glow-epic: rgba(138, 47, 126, 0.18);
    --rarity-glow-legendary: rgba(184, 145, 47, 0.35);
    --backdrop-filter: blur(22px) brightness(1.1) saturate(0.55) sepia(0.35);
    --backdrop-room-filter: blur(14px) brightness(1.1) saturate(0.6) sepia(0.3);
    --shadow-dialog: 0 18px 48px -20px rgba(58, 40, 18, 0.45);
    --log-opacity: 100%;
    --atmo-strength: 0.4;
    --atmo-vignette-rgb: 92, 62, 28;
    --backdrop-veil: 0.72;
    color-scheme: light;
}
/* An explicit "dark" under a light OS: the bare :root values above apply; this only pins the native controls. */
:root[data-theme="dark"] { color-scheme: dark; }

* { box-sizing: border-box; }
[hidden] { display: none !important; }

html { color-scheme: dark light; }

/* P13.1: the fluid type scale + adjustable UI scale, both in one place.
 * `clamp(14px, 0.25vw + 12px, 19px)` is the "auto" curve app.js's own
 * `autoUiScale` was fitted against (view.js): ~16px at a 1600px-ish
 * viewport (this file's own historical implicit baseline — every rem value
 * below was converted from its old px value by dividing by 16, so 1600px
 * wide at the default 100% scale repaints pixel-for-pixel identical to the
 * P10.x layout this replaced), sliding down to 14px by ~700px (where the
 * single-column mobile layout takes over anyway) and up to 19px by 2560px+
 * (BUILD_PLAN's own "measure at 1920x1080... and 2560x1440" sweep). Multiplying
 * by `--ui-scale` (a bare number, set on `documentElement` by app.js from
 * the Settings slider, 0.8-1.5) makes it the SAME lever whether the browser
 * gave the player a small window or they turned the dial themselves — a rem
 * anywhere in this file answers to both.
 */
html {
  font-size: calc(clamp(14px, 0.25vw + 12px, 19px) * var(--ui-scale));
  /* POST-CLIENT (the 3 px flake): the viewport's own overflow is set here, so body's is its own and not propagated. */
  overflow: hidden;
}

body {
  margin: 0;
  background: var(--bg);
  color: var(--ink);
  font-size: 1rem;
  line-height: 1.4;
  font-family: var(--sans);
  height: 100vh;
  height: 100dvh;
  /* POST-CLIENT: `clip`, not a scroller. P14.1's hit shake (.game-shake) moves #game up to 6 px sideways; while body's
     `hidden` went to the viewport, that moved box still counted as page overflow (fxFight's rare 3-5 px at 1366) and
     the page could be scrolled sideways by script or focus. Clipped here, the shake never reaches the page's width.
     `hidden` first for a browser without `clip`. */
  overflow: hidden;
  overflow: clip;
}

::selection { background: var(--focus); color: var(--well); }

/* Visible, high-contrast focus everywhere — never the browser default alone,
   never removed without a replacement. */
:focus { outline: none; }
:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
  border-radius: 2px;
}

/* --------------------------------------------------------------- motion */
.bar-fill { transition: none; }
@media (prefers-reduced-motion: no-preference) {
  .bar-fill { transition: width 180ms ease; }
}

/* ---------------------------------------------------------------- gate */
.gate {
  position: relative;
  display: grid;
  place-items: center;
  height: 100vh;
  height: 100dvh;
  padding: 16px;
  background: var(--bg);
  overflow: hidden;
}
/* P11.2: the splash, behind a dark overlay dark enough on its own — before
   `.gate-card`'s own OPAQUE `--panel` background is even considered — that
   text laid directly over it (there is none today, but nothing here should
   rely on the card to carry contrast alone) would still read at WCAG AA. A
   missing splash file (404) just leaves the plain gradient/`--bg` under it —
   no JS, nothing to catch, `background-image` degrades on its own. */
.gate::before {
  content: "";
  position: absolute;
  inset: 0;
  background-image:
    linear-gradient(180deg, rgba(4, 5, 9, 0.74), rgba(4, 5, 9, 0.86)),
    radial-gradient(ellipse at 50% -10%, color-mix(in srgb, var(--gold) 10%, transparent), transparent 55%),
    url("/art/splash.webp");
  background-size: cover;
  background-position: center;
  z-index: 0;
}
.gate-card {
  position: relative;
  z-index: 1;
  background: var(--panel);
  border: 1px solid var(--frame-line);
  border-radius: var(--radius);
  box-shadow: 0 0 0 1px var(--well), 0 18px 48px -20px rgba(0, 0, 0, 0.6);
  padding: 1.875rem 2rem;
  display: grid;
  gap: 0.875rem;
  width: min(23.75rem, 100%);
}
.gate-card h1 {
  margin: 0 0 4px;
  font: 700 1.1875rem/1.2 var(--sans);
  color: var(--gold);
  letter-spacing: 0.01em;
}
.gate-mark { color: var(--gold-dim); }
/* RENAME (Blind Scribes): the intro plate, inside `.gate-card` above its h1 (not `.gate::before`'s full-bleed
   background). Sized to the card's own content width (not a padding-bleeding negative margin, which would need
   retuning at every `.gate-card { padding }` breakpoint below, including the phone ones) so it just works at any
   width, keeps its 16:9 aspect, and never overflows the card's rounded corners. A missing/404 image just collapses
   (no `alt` box, no broken-image layout jump) since width/height reserve the space either way. */
.gate-splash {
  display: block;
  width: 100%;
  margin: 0 0 0.875rem;
  aspect-ratio: 1200 / 672;
  height: auto;
  object-fit: cover;
  border-radius: var(--radius);
}
/* "Show artwork" off (app.js `applySettings`, the same `body[data-art]` the room/portrait art already honours):
   hidden outright, not just transparent, so no empty gap or alt text shows above the title. */
body[data-art="false"] .gate-splash { display: none; }
.gate-prompt {
  margin: 0;
  color: var(--ink);
  white-space: pre-wrap;
  font: 0.84375rem/1.5 var(--mono);
}
.gate-note { margin: 0; color: var(--hurt); font-size: 0.78125rem; }
.gate-lines {
  margin: 0;
  max-height: 30vh;
  overflow-y: auto;
  font: 0.8125rem/1.5 var(--mono);
  background: var(--well);
  border: 1px solid var(--frame-line);
  border-radius: var(--radius);
}
.gate-lines:empty { display: none; border: 0; }
.gate-lines .entry { padding: 2px 10px; }
.gate-card input {
  font: 0.875rem var(--sans);
  padding: 10px 11px;
  background: var(--well);
  border: 1px solid var(--frame-line);
  border-radius: var(--radius);
  color: var(--ink-bright);
}
.gate-card input::placeholder { color: var(--ink-dim); }
/* `:not(.creation-card)`: without it, this rule's (class + element)
   specificity beats `.creation-card`'s (class only) below regardless of
   source order, so every class/race card — itself a `<button>` inside
   `.gate-card` — got submitted in the Send button's own gold gradient
   instead of the dark `--well` panel `.creation-card` asks for, with
   `--ink-dim` blurb text and `--gold` name text now unreadable against a
   background the same colour as the name. */
.gate-card button:not(.creation-card) {
  background: linear-gradient(180deg, var(--gold), var(--gold-dim));
  color: var(--well);
  border: 1px solid var(--gold-dim);
  border-radius: var(--radius);
  padding: 10px;
  font: 700 0.875rem var(--sans);
  cursor: pointer;
}
.gate-card button:not(.creation-card):hover { filter: brightness(1.08); }

/* P11.8: the class/race card grid (renderCreationCards, view.js). Widens the
   gate card only while a grid is actually showing — `.has-cards` is toggled
   in app.js — so the ordinary name/password steps keep the narrow 380px
   card. Colours reuse `.gate-card`'s own tokens throughout: WCAG AA against
   `--well` in both themes, same as the rest of the gate. */
.gate-card.has-cards { width: min(42.5rem, 100%); }
.gate-cards:empty { display: none; }
.creation-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
  gap: 10px;
  max-height: 46vh;
  overflow-y: auto;
}
.creation-card {
  /* A real <button>: without resetting native appearance, some platforms'
     own themed form-control chrome paints over (or under) the background
     below instead of the plain panel this needs — caught in the P11.8
     screenshot (docs/screenshots/creation.png) showing every card in the
     OS's own accent colour instead of `--well` until this row was added. */
  appearance: none;
  -webkit-appearance: none;
  display: grid;
  gap: 6px;
  align-content: start;
  background: var(--well);
  border: 1px solid var(--frame-line);
  border-radius: var(--radius);
  padding: 8px;
  text-align: left;
  cursor: pointer;
  color: inherit;
  font: inherit;
}
.creation-card:hover, .creation-card:focus-visible { border-color: var(--gold-dim); filter: brightness(1.08); outline: none; }
.creation-card-art { width: 100%; aspect-ratio: 1 / 1; object-fit: cover; border-radius: calc(var(--radius) - 2px); display: block; }
.creation-card-name { font: 700 0.8125rem var(--sans); color: var(--gold); }
.creation-card-blurb { font: 0.75rem/1.35 var(--sans); color: var(--ink-dim); }
/* PT follow-up: the telnet `?` facts on each card (session/creation.js classFacts/raceFacts). */
.creation-card-stats { font: 600 0.75rem var(--sans); color: var(--ink); }
.creation-card-detail { font: 0.75rem/1.35 var(--sans); color: var(--ink); }
.creation-card-detail code { font: 0.6875rem var(--mono, monospace); color: var(--gold); }
.creation-note { margin: 10px 0 0; font: 0.8125rem/1.4 var(--sans); color: var(--ink); }

/* ---------------------------------------------------------------- game */
.game {
  display: grid;
  /* log+side | P10.6's hotkey bar | input dock | P10.3's bottom bar, full width, under all three. */
  grid-template-rows: minmax(0, 1fr) auto auto auto;
  height: 100vh;
  height: 100dvh;
  background: var(--frame);
  padding: var(--gap);
  gap: var(--gap);
}

/* ---------------------------------------------- P10.4: player-arranged layout
 * log | (left column, hidden if empty) | log | splitter | right column —
 * a plain flex row rather than P10.1's fixed CSS grid, since a pane can now
 * move between columns: flexbox distributes each pane's own flex-basis
 * (content-sized vs "share the leftover space", below) the same way in
 * either column, with no per-column row template to keep in sync. Each
 * `.side-col`'s WIDTH is set inline by app.js from the saved layout
 * (`layout.leftWidth`/`rightWidth`) — CSS only supplies the default via
 * `width` below, immediately overridden on load.
 */
.layout {
  display: flex;
  gap: var(--gap);
  min-height: 0;
}

/* P14.1: day/night grading + dark-room vignette, over the WHOLE page (a
   fixed overlay rather than a per-pane filter, so the gate/settings dialog
   and every pane tint together as one scene). `pointer-events: none` so it
   never intercepts a click regardless of stacking order.
   NOTE: `--atmo-tint`/`--atmo-vignette` are plain (untyped) custom
   properties, and this background is a gradient, not `background-color` —
   neither animates via a CSS `transition` without `@property` (which this
   file does not register, to keep the change simple), so a phase/room
   change is a hard cut here, not a fade. `applyAtmosphere` (view.js) is
   called at most once per Room.Info/Char.Vitals, so the cut is infrequent
   enough this has not needed fixing yet. */
body::after {
  content: "";
  position: fixed;
  inset: 0;
  z-index: 500;
  pointer-events: none;
  background:
    radial-gradient(ellipse at center, transparent 40%, rgba(var(--atmo-vignette-rgb), var(--atmo-vignette)) 100%),
    linear-gradient(var(--atmo-tint), var(--atmo-tint));
  /* PTV-FIXES §3: the light theme grades at half strength with a warm sepia edge (a black edge and a full blue night
     wash turned the parchment grey); dark is unchanged (1, black). */
  opacity: var(--atmo-strength);
}

.log-wrap {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: var(--gap);
  min-height: 0;
  min-width: 0;
  flex: 1 1 auto;
}
/* P14.1: the living backdrop — the zone banner, blurred/darkened, filling
   `.log-wrap` behind everything else in it. `z-index: 0` on the wrap's
   first two children, `1`+ on the toolbar/log/jump-button (below), keeps
   the backdrop and weather layer strictly behind the readable UI. Scaled up
   past the blur radius so its own edges never show at the pane's edge. */
.log-backdrop {
  position: absolute;
  inset: 0;
  z-index: 0;
  overflow: hidden;
  border-radius: var(--radius);
  background: var(--panel);
}
.log-backdrop img {
  position: absolute;
  inset: -10%;
  width: 120%;
  height: 120%;
  object-fit: cover;
  filter: blur(38px) brightness(0.4) saturate(1.15);
  opacity: 0;
  transition: opacity 900ms ease;
}
/* P15: a room's own painted scene reads as a real place, not just an
   establishing shot for the zone — a lighter blur than the zone-banner
   fallback so it stays legible as a scene while still sitting well behind
   the readable UI (`renderLogBackdrop`, app.js, picks the class). */
.log-backdrop img.backdrop-room { filter: blur(16px) brightness(0.45) saturate(1.1); }
.log-backdrop img.backdrop-in { opacity: 1; }
@media (prefers-reduced-motion: reduce) {
  .log-backdrop img { transition: none; }
}
/* The log's own background goes translucent so the backdrop shows through
   it, not just through the gaps around it — falls back to the ordinary
   solid `--panel` look when `.log-backdrop` is empty (no image at all, art
   off or a zone with no banner), since an empty backdrop's own background
   is that same solid colour. */
.log-toolbar, #log { position: relative; z-index: 2; }
.weather-canvas {
  position: absolute;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  width: 100%;
  height: 100%;
}
.log-toolbar { display: flex; gap: 6px; align-items: center; }
.log-filter-input {
  flex: 1 1 auto;
  min-width: 0;
  background: var(--well);
  border: 1px solid var(--frame-line);
  border-radius: var(--radius);
  color: var(--ink-bright);
  font: 0.78125rem var(--sans);
  padding: 6px 9px;
}
.log-filter-input::placeholder { color: var(--ink-dim); }
.toolbar-btn {
  flex: 0 0 auto;
  background: var(--well);
  color: var(--ink);
  border: 1px solid var(--frame-line);
  border-radius: var(--radius);
  padding: 6px 10px;
  font: 700 0.75rem var(--sans);
  cursor: pointer;
  white-space: nowrap;
}
.toolbar-btn:hover { border-color: var(--gold-dim); color: var(--gold); }
/* P13.1: the Settings dialog's "Auto" UI-scale button (and any future
   toggle button) — a real `aria-pressed` state, not a bespoke class, so a
   screen reader gets the same information the border/text colour give
   sighted players. */
.toolbar-btn[aria-pressed="true"] { border-color: var(--gold-dim); color: var(--gold); background: color-mix(in srgb, var(--gold) 12%, var(--well)); }
.jump-bottom {
  position: absolute;
  /* P14.1: `.log-backdrop`/`.weather-canvas` sit at z-index 0/1 inside the
     same `.log-wrap`; this button must stay above both (it already sits
     above `.log` itself via document order + its own `position:absolute`,
     but an explicit z-index keeps that true regardless of order). */
  z-index: 2;
  right: 16px;
  bottom: 16px;
  background: var(--gold);
  color: var(--well);
  border: 1px solid var(--gold-dim);
  border-radius: 999px;
  padding: 7px 14px;
  font: 700 0.75rem var(--sans);
  cursor: pointer;
  box-shadow: 0 4px 14px -4px rgba(0, 0, 0, 0.55);
}
.jump-bottom:hover { filter: brightness(1.08); }

.side-col {
  display: flex;
  flex-direction: column;
  gap: var(--gap);
  min-height: 0;
  flex: 0 0 auto;
  /* P13.1: only the FIRST paint (or a no-JS view) ever shows this — app.js
     sets an inline px width from the saved/dragged layout (`leftWidth`/
     `rightWidth`, LAYOUT_MIN_WIDTH/LAYOUT_MAX_WIDTH in view.js still bound
     that drag in raw px, unrelated to the scale setting) immediately on
     load. Kept in rem with a min/max here anyway so that first paint, and
     the width the splitter drag falls open to before a save exists, both
     already respect the current type scale rather than assuming a small,
     fixed screen. */
  width: clamp(14rem, 18vw, 18rem);
  /* A scroll fallback, not the normal case: every pane below has its own
     min-height floor (its header) and the content-sized ones can shrink
     into their OWN internal scroll first — this only engages if even every
     header's floor together still doesn't fit the column's real height,
     so a tall enough window never shows this scrollbar at all. Plain
     `overflow: hidden` here was the P10.4 bug the CLAUDE.md control catalogue
     will want repeated: it silently clipped Talk/Arrivals/Deaths to nothing
     — not scrolled, not visible, gone — on a full-size desktop window
     whenever Room/Map/Character's own natural sizes left no leftover space
     for a `flex: 1 1 0` sibling to grow into. */
  overflow-y: auto;
  overflow-x: hidden;
}
/* The movable-pane list itself — plain within `#side-left`, nested inside
   `#side-right` below the pinned Vitals pane (`applyLayout`'s own two
   container arguments). */
.side-col-movable { display: flex; flex-direction: column; gap: var(--gap); min-height: 0; min-width: 0; flex: 1 1 auto; }

.splitter {
  flex: 0 0 6px;
  cursor: col-resize;
  position: relative;
}
.splitter::after { content: ""; position: absolute; inset: 0 2px; background: var(--frame-line); border-radius: 2px; }
.splitter:hover::after, .splitter:focus-visible::after { background: var(--gold-dim); }

/* room, map, char (content-sized, but ALLOWED TO SHRINK — `flex: 0 1 auto`,
   not `0 0 auto` — into their own internal `overflow-y: auto`, below) vs.
   Talk/Arrivals/Deaths (share whatever space is left over, `flex: 1 1 0`)
   — by `data-pane`, not by column or position, so the rule holds
   regardless of which column a pane is currently in or how it was
   reordered. Every pane gets the SAME `min-height` floor: its own header's
   height (~34px, `.pane-head-row`'s real content plus padding/border) —
   without it, a `flex: 1 1 0` pane can shrink not just to its content but
   all the way to zero, taking its header (and every move/collapse control
   in it) with it; `0 1 auto` on the others means a tight column shrinks
   Room/Map/Character toward this same floor and lets their OWN scrollbars
   do the rest, rather than starving a Talk/Arrivals/Deaths sibling of
   space it would otherwise get. Collapsed overrides either: a collapsed
   pane is always content-sized (its header alone), the more specific
   selector below wins over both. */
/* PTV-FIXES2 §6: `:where(.pane)` — the move buttons carry `data-pane` too, and must not take a pane's flex or floor. */
:where(.pane)[data-pane="room"], :where(.pane)[data-pane="map"], :where(.pane)[data-pane="char"] { flex: 0 1 auto; min-height: 34px; }
/* P13.1: Vitals is pinned and NOT one of the shrink-into-your-own-scrollbar
   panes above (index.html's own comment: "the one always-visible HUD
   element") — `0 1 auto` let it shrink BELOW its own four-bar content
   height whenever the column had less to give than every pane's floor
   added up to. Measured present at EVERY desktop size tried (1366x768
   through 2560x1440, not just a short window): the plain 1920x1080
   screenshot already showed "ROOM" ghosted under the last vitals bar
   before this fix, just faintly enough to miss at a glance — a mid-fight
   4th bar (the opponent) made the column that much taller than a 3-bar
   idle Vitals, which was apparently enough on its own. `.pane-static`'s
   content row (`minmax(0, 1fr)`) has no floor of its own once a pane
   shrinks past its content, so the bars did not scroll or clip — they
   painted straight through into the Room pane's header below. `0 0 auto`
   (never shrink) fixes it the same way the room/map/char comment above
   already relies on for ITS floor: something else has to give first, and
   the column's own `overflow-y: auto` (`.side-col`) is exactly the
   fallback already documented for this case. */
[data-pane="vitals"] { flex: 0 0 auto; min-height: 34px; }
:where(.pane)[data-pane="talk"], :where(.pane)[data-pane="arrivals"], :where(.pane)[data-pane="deaths"] { flex: 1 1 0; min-height: 34px; }
.side-col [data-collapsed="true"] { flex: 0 0 auto; }

.pane, .pane-static {
  background: var(--panel);
  border: 1px solid var(--frame-line);
  border-radius: var(--radius);
  display: grid;
  grid-template-rows: auto minmax(0, 1fr);
  min-height: 0;
  /* P13.1: measured — a grid item's implicit `min-width: auto` sizes it to
     its content's own intrinsic minimum, same as flex; the Character pane's
     tab strip and table cells never NEEDED to wrap on the old, always-wide
     P10.x sidebar, so nothing here ever set this. Once the sidebar itself
     could be narrower (this file's own fluid `.side-col`, or just a modest
     scale/window), that intrinsic width made the whole pane wider than the
     column — `.side-col`'s `overflow-x: hidden` then clipped it at the
     column's true edge, so "SCORE" (the tab, `.char-tab`'s own ellipsis
     fix does nothing if this box is already too wide to reach) silently
     lost its last two letters with no ellipsis, no scrollbar, nothing to
     tell you a fourth letter existed. `min-width: 0` lets the pane itself
     shrink to the column instead, so wrapping/ellipsis/scroll inside it —
     already handled per-element — actually gets to run.
  */
  min-width: 0;
}
.pane[data-collapsed="true"], .pane-static[data-collapsed="true"] { grid-template-rows: auto; }
.pane[data-collapsed="true"] > :not(header), .pane-static[data-collapsed="true"] > :not(header) { display: none; }
.pane-head-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 6px;
  border-bottom: 1px solid var(--frame-line);
  padding-right: 4px;
}
.pane-head {
  padding: 5px 10px;
  font: 700 0.75rem var(--sans);
  color: var(--ink-dim);
  text-transform: uppercase;
  letter-spacing: 0.06em;
}
.pane-toggle {
  flex: 1 1 auto;
  min-width: 0;
  text-align: left;
  background: none;
  border: 0;
  color: var(--ink-dim);
  font: 700 0.75rem var(--sans);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  padding: 5px 10px;
  cursor: pointer;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.pane-toggle:hover { color: var(--ink); }
.pane-toggle::before { content: "\25be  "; }
[data-collapsed="true"] .pane-toggle::before { content: "\25b8  "; }
.pane-log { overflow-y: auto; padding: 4px 10px; font: 0.78125rem/1.5 var(--mono); min-height: 0; }
.pane-log .entry { padding: 1px 0; }

/* P10.4: per-pane layout controls — move to the other column, reorder up/
   down. Real buttons (never drag-only), so Tab + Enter/Space works the same
   as a click; `:disabled` (app.js sets it at either end of a column) reads
   the boundary instead of silently doing nothing. Hidden under ~700px along
   with everything else layout-editing related (see the responsive block). */
.pane-controls { display: flex; gap: 2px; flex: 0 0 auto; padding: 3px 2px; }
.pane-ctrl {
  width: 20px;
  height: 20px;
  padding: 0;
  line-height: 1;
  font: 0.6875rem var(--sans);
  background: var(--well);
  color: var(--ink-dim);
  border: 1px solid var(--frame-line);
  border-radius: 3px;
  cursor: pointer;
}
.pane-ctrl:hover:not(:disabled) { color: var(--ink); border-color: var(--gold-dim); }
.pane-ctrl:disabled { color: var(--frame-line); cursor: default; }

/* --------------------------------------------------------------- room */
/* P11.2: the zone banner (renderZoneBanner, view.js) — its own element
   (#room-banner), never torn down by room-panel's own replaceChildren, so
   it only visibly changes when the zone actually does. `:empty` (no art, or
   the toggle off) collapses it to nothing rather than an empty grey box. */
.room-banner { line-height: 0; }
.room-banner:empty { display: none; }
.room-banner .zone-banner { display: block; width: 100%; height: 5.25rem; object-fit: cover; border-radius: var(--radius) var(--radius) 0 0; }
.room-body { overflow-y: auto; max-height: 32vh; padding: 8px 10px; min-height: 0; font: 0.8125rem/1.5 var(--sans); }
.room-title { color: var(--gold); font-weight: 700; margin-bottom: 8px; }
.room-exits { display: flex; flex-wrap: wrap; gap: 5px; }
.exit-btn {
  font: 0.78125rem var(--sans);
  background: var(--well);
  color: var(--exit);
  border: 1px solid var(--frame-line);
  border-radius: var(--radius);
  padding: 4px 8px;
  cursor: pointer;
}
.exit-btn:hover { background: color-mix(in srgb, var(--exit) 16%, var(--well)); border-color: var(--exit); }
.exit-btn.exit-closed { color: var(--ink-dim); font-style: italic; }

/* ---------------------------------------------- P10.4: room-entity menus
 * Room.Info's creatures/items/players (roomCreaturesInfo/roomItemsInfo/
 * roomPlayersInfo), each a native <details> disclosure — no positioning
 * logic, keyboard-operable for free (Enter/Space toggles a focused
 * <summary>). "Kept simple", the survey's own verdict (client-survey.md
 * "What we'll take" #7).
 */
.room-occupants { margin-top: 8px; display: grid; gap: 8px; }
.room-section-label { font: 700 0.6875rem var(--sans); color: var(--ink-dim); text-transform: uppercase; letter-spacing: 0.06em; margin-bottom: 3px; }
.room-entity { border: 1px solid var(--frame-line); border-radius: var(--radius); background: var(--well); margin-bottom: 4px; }
.room-entity:last-child { margin-bottom: 0; }
.room-entity summary {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 5px 8px;
  cursor: pointer;
  font: 0.78125rem var(--sans);
  color: var(--ink);
  list-style: none;
}
.room-entity summary::-webkit-details-marker { display: none; }
.room-entity summary::before { content: "\25b8  "; color: var(--ink-dim); }
.room-entity[open] summary::before { content: "\25be  "; }
.room-entity summary:hover { color: var(--ink-bright); }
.room-entity-actions { display: flex; flex-wrap: wrap; gap: 5px; padding: 0 8px 8px; }

/* ------------------------------------------------------- P11.2: artwork
 * Every image on the page (zone banner, room-panel/char-panel thumbnails,
 * the enemy-bar portrait) is an `<img>` whose `src` came from
 * `art/manifest.json` through `artUrl` (view.js) — an id, never server
 * text. `.art-fade` fades an image in on arrival, same reduced-motion
 * convention as `.bar-fill` above: the animation only runs when the visitor
 * has NOT asked for reduced motion, so the base rule is "no animation".
 * `.entity-thumb-fallback` is the silhouette CSS draws on its own — a
 * missing/unknown id, or the settings toggle off, never leaves a broken
 * `<img>` icon or an empty gap where a thumbnail was expected.
 */
.art-fade { opacity: 1; }
@media (prefers-reduced-motion: no-preference) {
  .art-fade { opacity: 0; animation: art-fade-in 260ms ease forwards; }
  @keyframes art-fade-in { to { opacity: 1; } }
  /* PTV-FIXES §2: Effects "off" removes every animation (below), and the fade's start (opacity 0) then stayed for
     good: every painting and portrait — the foe card's too — was invisible. Off means shown at once. */
  body[data-effects="off"] .art-fade { opacity: 1; }
}

.entity-thumb-wrap { display: inline-flex; flex-shrink: 0; }
.entity-thumb-wrap:empty { display: none; }
.entity-thumb { border-radius: 3px; object-fit: cover; background: var(--well); }
.entity-thumb-mob, .entity-thumb-fallback-mob { width: 3rem; height: 3rem; border-radius: 50%; }
.entity-thumb-item, .entity-thumb-fallback-item { width: 1.375rem; height: 1.375rem; }
.entity-thumb-fallback {
  display: block;
  background: var(--well);
  border: 1px dashed var(--frame-line);
  position: relative;
}
.entity-thumb-fallback::after {
  content: "?";
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  color: var(--ink-dim);
  font: 700 0.6875rem var(--sans);
}

/* The enemy bar's own larger portrait (renderTargetBar, view.js) — same
   thumbnail/fallback markup as the room panel, just scaled up here. */
.foe-portrait .entity-thumb, .foe-portrait .entity-thumb-fallback { width: 2.5rem; height: 2.5rem; border-radius: 50%; }
.foe-portrait { display: inline-flex; margin-right: 6px; position: relative; }

/* P14.1: the portrait desaturates and cracks as the fight goes against it
   (`foePortraitClass`, view.js — pure hp-fraction thresholds), and fades on
   death. `filter`/`opacity` only, so it costs nothing extra to draw. */
.foe-portrait-hurt .entity-thumb img, .foe-portrait-hurt .entity-thumb-fallback { filter: saturate(0.55) contrast(1.05); }
.foe-portrait-critical .entity-thumb img, .foe-portrait-critical .entity-thumb-fallback { filter: saturate(0.15) contrast(1.15) brightness(0.85); }
.foe-portrait-critical::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: 50%;
  background:
    linear-gradient(105deg, transparent 46%, rgba(0, 0, 0, 0.55) 48%, transparent 50%),
    linear-gradient(20deg, transparent 60%, rgba(0, 0, 0, 0.5) 62%, transparent 64%);
  pointer-events: none;
}
.foe-portrait-dead { opacity: 0.35; filter: grayscale(1); }
@media (prefers-reduced-motion: no-preference) {
  .foe-portrait-dead { transition: opacity 700ms ease, filter 700ms ease; }
}

/* P14.1: a floating combat number over the portrait — "dealt" (green/white,
   damage the player just did) vs "taken" (red, damage the player just
   suffered). Positioned by app.js's own spawn point; the float+fade is a
   pure CSS animation the caller's `setTimeout` removes the node after
   (`removeFloater`, view.js) — reduced motion drops the animation but the
   number is information, not motion, so it is never itself hidden. */
.dmg-floater {
  position: absolute;
  left: 50%;
  top: -4px;
  transform: translateX(-50%);
  font: 700 0.8125rem var(--sans);
  white-space: nowrap;
  pointer-events: none;
  text-shadow: 0 1px 3px rgba(0, 0, 0, 0.8);
}
.dmg-floater-dealt { color: var(--gain); }
.dmg-floater-taken { color: var(--hurt); }
@media (prefers-reduced-motion: no-preference) {
  .dmg-floater { animation: dmg-float 900ms ease-out forwards; }
}
@keyframes dmg-float {
  0% { opacity: 0; transform: translate(-50%, 6px) scale(0.9); }
  15% { opacity: 1; transform: translate(-50%, -6px) scale(1.05); }
  100% { opacity: 0; transform: translate(-50%, -34px) scale(1); }
}

/* P14.1: a brief shake/flash on a big hit — `shouldShake` (view.js) decides
   whether to add this class at all (off under reduced motion or below
   Effects=full); the class itself is the only thing app.js toggles. */
@media (prefers-reduced-motion: no-preference) {
  .game-shake { animation: game-shake 320ms ease-in-out; }
}
@keyframes game-shake {
  0%, 100% { transform: translate(0, 0); }
  20% { transform: translate(-6px, 2px); }
  40% { transform: translate(5px, -2px); }
  60% { transform: translate(-4px, 1px); }
  80% { transform: translate(3px, -1px); }
}
.room-entity-action {
  font: 0.75rem var(--sans);
  background: var(--panel);
  color: var(--exit);
  border: 1px solid var(--frame-line);
  border-radius: var(--radius);
  padding: 3px 8px;
  cursor: pointer;
}
.room-entity-action:hover { background: color-mix(in srgb, var(--exit) 16%, var(--panel)); border-color: var(--exit); }

/* -------------------------------------------------------------- vitals */
.vitals { padding: 8px 10px; display: grid; gap: 5px; }
.bar { position: relative; height: 20px; background: var(--bar-well); border: 1px solid var(--frame-line); border-radius: 3px; overflow: hidden; }
.bar-fill { position: absolute; inset: 0 auto 0 0; }
.bar-hp .bar-fill, .bar-foe .bar-fill { background: var(--hp); }
.bar-mana .bar-fill { background: var(--mana); }
.bar-moves .bar-fill { background: var(--moves); }

/* P10.6 (item 7): combat feel. A static (non-motion) low-HP indicator
   always applies, regardless of the reduced-motion preference — only the
   PULSE/FLASH animations themselves are gated below, so the warning is
   still visible with reduced motion, just not moving. `outline` rather than
   `box-shadow` because `.bar` has its own `overflow: hidden` for the fill. */
.bar-hp.hp-low { outline: 2px solid var(--hurt); outline-offset: 1px; }
@media (prefers-reduced-motion: no-preference) {
  .bar-hp.hp-low { animation: hp-pulse 1.4s ease-in-out infinite; }
  .bar-hp.hp-hit { animation: hp-hit-flash 0.5s ease-out; }
  .sb-target.round-tick .bar-foe { animation: round-tick-pulse 0.4s ease-out; }
}
@keyframes hp-pulse {
  0%, 100% { outline-color: var(--hurt); outline-offset: 1px; }
  50% { outline-color: transparent; outline-offset: 3px; }
}
@keyframes hp-hit-flash {
  0% { outline: 3px solid var(--hurt); outline-offset: 0; }
  100% { outline: 3px solid transparent; outline-offset: 3px; }
}
@keyframes round-tick-pulse {
  0% { outline: 2px solid var(--tick); outline-offset: 0; }
  100% { outline: 2px solid transparent; outline-offset: 4px; }
}
.bar-label {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 5px;
  padding: 0 8px;
  font: 0.71875rem/1 var(--sans);
  color: var(--bar-ink);
  text-shadow: 0 1px 2px rgba(0, 0, 0, 0.6);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* ----------------------------------------------------------- P10.2: map */
.room-map {
  padding: 8px 10px;
  display: flex;
  justify-content: center;
  min-height: 0;
  /* P14.1: fog-of-war — an unvisited, unknown cell already draws nothing
     (`renderRoomMap`'s own comment); this radial fog behind the whole grid
     just makes the unexplored void read as fog rather than empty page
     background, without changing what data is or isn't drawn. */
  background: radial-gradient(circle, transparent 45%, color-mix(in srgb, var(--well) 70%, transparent) 100%);
  border-radius: var(--radius);
}
.map-svg { display: block; max-width: 100%; height: auto; }
.map-room { stroke-width: 1.5; }
.map-room-known { fill: transparent; stroke: var(--ink-dim); stroke-dasharray: 2 2; }
.map-room-visited { fill: var(--well); stroke: var(--exit); }
.map-room-player { fill: var(--gold-dim); stroke: var(--gold); stroke-width: 2.5; }
.map-link { stroke: var(--frame-line); stroke-width: 2; }
.map-marker { stroke: none; }
.map-marker-up { fill: var(--gain); }
.map-marker-down { fill: var(--hurt); }

/* P14.1: a hostile creature in a visited room the player's own scan already
   reported (`cell.hostile`, core/commands/map.ts) pulses red; the player's
   own dot gets a soft gold glow ring so it reads as "you" among the others.
   Pure CSS animation — `map-hostile-dot`/`map-player-glow`'s own EXISTENCE
   is the only decision `renderRoomMap` makes (view.js); reduced motion just
   drops the pulse, the dot itself stays visible as a static warning. */
.map-hostile-dot { fill: var(--hurt); pointer-events: none; }
@media (prefers-reduced-motion: no-preference) {
  .map-hostile-dot { animation: map-hostile-pulse 1.6s ease-in-out infinite; }
}
@keyframes map-hostile-pulse {
  0%, 100% { opacity: 1; r: 3; }
  50% { opacity: 0.45; r: 4; }
}
.map-player-glow { fill: none; stroke: var(--gold); stroke-width: 1; opacity: 0.55; filter: blur(2px); pointer-events: none; }
@media (prefers-reduced-motion: no-preference) {
  .map-player-glow { animation: map-player-glow-pulse 2.2s ease-in-out infinite; }
}
@keyframes map-player-glow-pulse {
  0%, 100% { opacity: 0.4; }
  50% { opacity: 0.75; }
}

/* ---------------------------------------------------------- P10.2: char */
.char-panel { display: flex; flex-direction: column; min-height: 0; min-width: 0; }
.char-tabs { display: flex; border-bottom: 1px solid var(--frame-line); }
.char-tab {
  flex: 1;
  /* P13.1: measured — three equal `flex: 1` tabs with no floor default to a
     min-content width from their own longest word ("EQUIPMENT"); once the
     column is narrower than all three of those combined, `.side-col`'s own
     `overflow-x: hidden` (above) doesn't wrap or shrink them, it just hard-
     clips whichever tab lands past the edge — "SCORE" silently became
     "SCOR" in the 1920x1080 screenshot with nothing telling you a fourth
     letter existed. `min-width: 0` lets flex actually shrink the tab past
     its content; the ellipsis then shows something is truncated instead of
     a plain crop, at any column width or scale. */
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  background: none;
  border: 0;
  border-bottom: 2px solid transparent;
  color: var(--ink-dim);
  font: 700 0.75rem var(--sans);
  text-transform: uppercase;
  letter-spacing: 0.05em;
  padding: 0.4375rem 0.25rem;
  cursor: pointer;
}
.char-tab:hover { color: var(--ink); }
.char-tab.active { color: var(--gold); border-bottom-color: var(--gold); }
.char-tab-body { overflow-y: auto; max-height: 26vh; padding: 8px 10px; min-height: 0; font: 0.78125rem var(--sans); }
.char-tab-footer { margin-top: 6px; }
.char-table { width: 100%; border-collapse: collapse; }
.char-table td { padding: 2px 4px; vertical-align: middle; }
.char-table td.right { text-align: right; }
/* P11.2: Inventory/Equipment icons — a row's name cell already reads left to
   right as [icon][name]; the equipment value cell stays right-aligned as a
   whole (icon first, same reading order) rather than the icon floating to
   the far right on its own. */
.char-table td:has(.entity-thumb-wrap) { display: flex; align-items: center; gap: 6px; }
.char-table td.right:has(.entity-thumb-wrap) { justify-content: flex-end; }
.muted { color: var(--ink-dim); }
.broken { color: var(--hurt); }

/* Item rarity (mud-engine's own palette, carried over for the inventory tab). */
.rarity-common { color: var(--ink); }
.rarity-uncommon { color: var(--gain); }
.rarity-rare { color: var(--focus); }
.rarity-epic { color: var(--s-tell); }
.rarity-legendary { color: var(--gold); font-weight: 700; }

/* ---------------------------------------------------------------- log */
.log {
  /* P14.1: translucent rather than solid `--panel`, so `.log-backdrop`
     (behind it, same stacking context) shows through the log text area
     too, not just the gaps around it. Reads identically to the old solid
     colour when the backdrop is empty, since its own fallback background
     is that same `--panel`. Lowered from 82% (a user's own request for a
     bit more see-through) -- still opaque enough that dense combat text
     stays readable over a bright backdrop image. */
  background: color-mix(in srgb, var(--panel) 72%, transparent);
  border: 1px solid var(--frame-line);
  border-radius: var(--radius);
  padding: 10px 14px;
  overflow-y: auto;
  min-height: 0;
  white-space: pre-wrap;
  font: 0.90625rem/1.55 var(--mono);
  /* P13.1 capped this at 100ch, centered, to keep very wide monitors from
     reading as a wall of text. Removed on a user's own explicit request
     (the gutters either side read as wasted space, not as a feature) --
     the log now fills whatever width `.log-wrap` gives it, same as every
     other pane. */
  width: 100%;
}
.log .entry { margin-bottom: 2px; }
.log .entry.cmd { color: var(--gold); margin: 10px 0 3px; }
/* PTV-FIXES2 §8: a line laid out in columns keeps its spacing but WRAPS when it is wider than the log, rather than
   scrolling sideways on its own: 100+ unfocusable scroll regions after an hour of play (axe scrollable-region-focusable),
   and making each one a tab stop would bury the command line. */
.log .entry.tabular { white-space: pre-wrap; overflow-wrap: anywhere; }

/* P10.4: Settings' "timestamps on log lines" — app.js stamps each entry's
   own `data-time` at append time (never rewriting `storyEntry`'s escaped
   `textContent`, so the filter's own text match stays exactly what the
   server sent); this just decides whether the stamp is ever painted, so
   toggling the setting is retroactive over every line already in the log. */
body[data-timestamps="true"] .log .entry[data-time]::before {
  content: attr(data-time) " ";
  color: var(--ink-dim);
  font-size: 0.85em;
}

/* combat sub-kinds classify() still reads from the text itself, because
   "a goblin dies!" and "you hit the goblin" share one stream (combat) and
   nothing server-side splits them further (view.js's own comment). */
.log .entry.you { color: var(--ink-bright); }
.log .entry.hurt { color: var(--hurt); }
.log .entry.gain { color: var(--gain); }
.log .entry.death { color: var(--death); font-weight: 700; }
.log .entry.defeat {
  color: var(--ink-bright);
  background: color-mix(in srgb, var(--hp) 22%, transparent);
  border-left: 3px solid var(--hurt);
  padding: 4px 8px;
  margin: 4px 0;
  font-weight: 700;
}

/* stream classes (view.js STREAM_CLASS / streamClass): colour from the
   server's own `stream` field, never from parsing the line's words. */
.s-tell { color: var(--s-tell); }
.s-gossip { color: var(--s-gossip); }
.s-shout { color: var(--s-shout); }
.s-talk { color: var(--s-talk); }
.s-group { color: var(--s-group); }
.s-system { color: var(--s-system); font-style: italic; }
.s-deaths { color: var(--s-deaths); font-weight: 700; }

/* P10.7: enemy highlight — spans on the room/Also-here/Danger lines ONLY
   (view.js storyEntry renders `OutputLine.spans` as child <span>s; `kind` is
   set server-side from what a thing IS — never from parsing the line's own
   words, and never from anything a player typed). `--hurt` is already the
   WCAG-checked high-contrast warning colour this file uses for "you take
   damage" in both themes; reused here so hostile reads as the same danger
   colour throughout, rather than adding a second one. */
.span-hostile {
  color: var(--hurt);
  font-weight: 700;
}
.span-hostile::before { content: "\2694\fe0e "; } /* crossed swords, text-style selector: a plain glyph, not an emoji-coloured one */
/* P11.5: elite outranks plain hostile (view.js/look.ts put `kind: "elite"` ahead of `"hostile"`) — reuses --xp, already the WCAG-checked purple this file uses for "special" (the XP bar), so an elite reads as its own colour rather than a louder red. */
.span-elite {
  color: var(--xp);
  font-weight: 700;
}
.span-elite::before { content: "\2605\fe0e "; } /* star, text-style selector: a plain glyph, not an emoji-coloured one */
.span-creature { color: var(--ink-bright); }
.span-player { color: var(--s-group); font-weight: 600; }

/* --------------------------------------------------------------- dock */
.dock { display: grid; gap: var(--gap); }
.prompt {
  display: grid;
  grid-template-columns: auto 1fr;
  align-items: center;
  background: var(--well);
  border: 1px solid var(--frame-line);
  border-radius: var(--radius);
}
.prompt .caret { color: var(--gold); font-weight: 700; padding: 0 4px 0 10px; font: 0.9375rem var(--mono); }
.prompt input {
  width: 100%;
  border: 0;
  background: transparent;
  color: var(--ink-bright);
  font: 0.9375rem var(--mono);
  padding: 9px 10px 9px 2px;
  outline: none;
}
.prompt:focus-within { outline: 2px solid var(--focus); outline-offset: -1px; border-radius: var(--radius); }

/* -------------------------------------------------- P10.3: bottom status bar
 * StormFront/MUSHclient-style: full width, dense, below the log and the
 * input dock both. A single flex row that wraps section-by-section, never
 * mid-section (each section is its own flex item), so a narrow window folds
 * to a compact multi-row strip instead of clipping content.
 */
.statusbar {
  display: flex;
  flex-direction: column;
  gap: 6px;
  background: var(--panel);
  border: 1px solid var(--frame-line);
  border-radius: var(--radius);
  padding: 6px 10px;
  font: 0.78125rem var(--sans);
}
.sb-row { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 16px; }

.sb-vitals { display: flex; gap: 5px; flex: 2 1 340px; min-width: 220px; }
.sb-vitals .bar, .sb-target .bar, .sb-group .bar { height: 18px; flex: 1; min-width: 70px; }
.bar-xp .bar-fill { background: var(--xp); }
.bar-group .bar-fill { background: var(--group); }

.sb-target { display: flex; gap: 5px; flex: 1 1 140px; min-width: 0; }
.sb-target:empty { flex-basis: 0; min-width: 0; }
/* THE ROUND, Q0: the roundtime bar (view.js renderRoundtimeBar), hidden at 0 by [hidden]. */
.sb-rt { display: flex; flex: 0 1 140px; min-width: 0; }
.sb-rt .bar { height: 18px; flex: 1; min-width: 70px; }
.bar-rt .bar-fill { background: var(--rt-fill, #c9a227); transition: width 1.9s linear; }

.sb-chips { display: flex; flex-wrap: wrap; gap: 4px; flex: 3 1 220px; align-items: center; }
.chip {
  background: var(--chip-bg);
  color: var(--chip-ink);
  border: 1px solid var(--frame-line);
  border-radius: 999px;
  padding: 2px 8px;
  font: 0.6875rem var(--sans);
  white-space: nowrap;
}
.chip-standing, .chip-resting, .chip-sleeping, .chip-meditating { border-color: var(--chip-posture); color: var(--chip-posture); }
.chip-stance { border-color: var(--chip-stance); color: var(--chip-stance); }
.chip-combat { border-color: var(--chip-combat); color: var(--chip-combat); font-weight: 700; }
.chip-link { border-color: var(--chip-link); color: var(--chip-link); }
.chip-afk { border-color: var(--chip-afk); color: var(--chip-afk); }
.chip-grouped { border-color: var(--chip-grouped); color: var(--chip-grouped); }
.chip-wimpy { border-color: var(--chip-wimpy); color: var(--chip-wimpy); }
.chip-effect { border-color: var(--chip-effect); color: var(--chip-effect); }

/* The world-tick sweep: a thin bar that fills over TICK_INTERVAL_MS (2 s),
   restarted by app.js on each `World.Tick` message (toggling `.sweeping`
   forces the animation to replay from 0). Reduced motion disables it
   entirely — the clock/weather text beside it still updates either way. */
.sb-tick { display: flex; flex-direction: column; gap: 3px; flex: 1 1 110px; min-width: 90px; }
.sb-tick-track { height: 4px; background: var(--bar-well); border-radius: 2px; overflow: hidden; }
.tick-sweep { height: 100%; width: 0%; background: var(--tick); }
@media (prefers-reduced-motion: no-preference) {
  .tick-sweep.sweeping { animation: tick-sweep-anim 2s linear; }
}
@keyframes tick-sweep-anim {
  from { width: 0%; }
  to { width: 100%; }
}
.sb-clock { color: var(--ink-dim); font-size: 0.6875rem; white-space: nowrap; }

.sb-compass-wrap { display: flex; align-items: center; gap: 8px; flex: 0 0 auto; }
.compass-grid { display: grid; grid-template-columns: repeat(3, 1.375rem); grid-template-rows: repeat(3, 1.375rem); gap: 2px; }
.compass-cell { display: block; }
.compass-dir {
  width: 1.375rem;
  height: 1.375rem;
  padding: 0;
  font: 700 0.625rem var(--sans);
  border-radius: 3px;
  border: 1px solid var(--frame-line);
  background: var(--well);
  color: var(--compass-dark);
  cursor: pointer;
}
.compass-dir.compass-lit { color: var(--exit); border-color: var(--exit); cursor: pointer; }
.compass-dir.exit-closed { color: var(--ink-dim); border-style: dashed; }
.compass-dir.compass-dark { color: var(--compass-dark); cursor: default; }
.compass-vertical { display: flex; flex-direction: column; gap: 2px; }
.sb-meta { display: flex; flex-direction: column; gap: 1px; font-size: 0.6875rem; color: var(--ink-dim); white-space: nowrap; }
.sb-level { color: var(--gold); font-weight: 700; }

.sb-group { display: flex; flex-wrap: wrap; gap: 5px; flex: 2 1 220px; min-width: 160px; }
.sb-group .sb-empty { font-size: 0.6875rem; }

/* ------------------------------------------------------ P10.4: settings
 * font size / density / bottom-bar on-off, applied as attributes on <body>
 * (app.js `applySettings`, from `loadSettings`/Settings' own controls) so
 * every rule they touch lives in one place, same convention as the theme
 * override above (`[data-theme]`). Timestamps' own rule sits with the log
 * (`.log .entry[data-time]::before`, above) since it touches nothing else.
 */
body[data-font-size="small"] { font-size: 0.78125rem; }
body[data-font-size="large"] { font-size: 0.96875rem; }
body[data-font-size="small"] .log { font-size: 0.8125rem; }
body[data-font-size="large"] .log { font-size: 1rem; }

body[data-density="compact"] { --gap: 0.1875rem; }
body[data-density="compact"] .log { padding: 6px 10px; }
body[data-density="compact"] .log .entry { margin-bottom: 0; }
body[data-density="compact"] .pane-log { padding: 2px 8px; }
body[data-density="compact"] .room-body, body[data-density="compact"] .char-tab-body { padding: 5px 8px; }
body[data-density="compact"] .pane-toggle, body[data-density="compact"] .pane-head { padding: 3px 8px; }

/* A native <dialog>: the browser supplies the modal backdrop, focus trap
   and Esc-to-close — nothing here reimplements any of that. */
.settings-dialog {
  background: var(--panel);
  color: var(--ink);
  border: 1px solid var(--frame-line);
  border-radius: var(--radius);
  padding: 1.25rem 1.375rem;
  width: min(24rem, 92vw);
  /* P10.6 added several rows (keypad walk, sound, notifications, the
     highlight/gag editor) — the dialog now scrolls its own content rather
     than growing past a short viewport; the header/actions are not sticky,
     kept simple per client-survey.md's own "no construction kit" rule. */
  max-height: min(35rem, 88vh);
  overflow-y: auto;
  box-shadow: 0 18px 48px -20px rgba(0, 0, 0, 0.6);
}
.settings-dialog::backdrop { background: rgba(4, 6, 10, 0.55); }
.settings-title { margin: 0 0 14px; font: 700 1rem var(--sans); color: var(--gold); }
.settings-subtitle { margin: 4px 0 0; font: 700 0.8125rem var(--sans); color: var(--gold-dim); }
.settings-divider { border: none; border-top: 1px solid var(--frame-line); margin: 4px 0; }
.settings-form { display: grid; gap: 12px; }
.settings-row { display: flex; align-items: center; justify-content: space-between; gap: 12px; font: 0.8125rem var(--sans); }
.settings-row select {
  background: var(--well);
  color: var(--ink-bright);
  border: 1px solid var(--frame-line);
  border-radius: var(--radius);
  padding: 5px 8px;
  font: 0.8125rem var(--sans);
}
.settings-row-check { justify-content: flex-start; }
/* P13.1: the UI-scale row is the one settings row with three controls
   (slider, live %, Auto button) instead of one — `flex-wrap` so a narrow
   dialog (the mobile screenshot's own width) wraps the control group to a
   second line under its own label rather than squeezing the slider to
   nothing. */
.settings-row-scale { align-items: flex-start; flex-wrap: wrap; }
.scale-control { display: flex; align-items: center; gap: 8px; flex: 1 1 auto; min-width: 0; justify-content: flex-end; }
.scale-control input[type="range"] { flex: 1 1 auto; min-width: 5rem; accent-color: var(--gold); }
.scale-value { color: var(--ink-dim); font: 0.75rem var(--mono); min-width: 2.6em; text-align: right; }
.settings-actions { display: flex; justify-content: flex-end; margin-top: 4px; }

/* ------------------------------------------------- P10.6: highlights & gags */
.highlight-list { display: grid; gap: 6px; max-height: 140px; overflow-y: auto; }
.highlight-row {
  display: flex;
  align-items: center;
  gap: 8px;
  font: 0.78125rem var(--mono);
  background: var(--well);
  border: 1px solid var(--frame-line);
  border-radius: var(--radius);
  padding: 4px 8px;
}
.highlight-summary { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.highlight-swatch { font-weight: 700; }
.highlight-swatch.hl-red { color: var(--hurt); }
.highlight-swatch.hl-yellow { color: var(--death); }
.highlight-swatch.hl-green { color: var(--gain); }
.highlight-swatch.hl-blue { color: var(--mana); }
.highlight-swatch-gag { color: var(--ink-dim); text-decoration: line-through; }
.highlight-form { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; font: 0.78125rem var(--sans); }
.highlight-form input[type="text"] {
  flex: 1;
  min-width: 120px;
  background: var(--well);
  color: var(--ink-bright);
  border: 1px solid var(--frame-line);
  border-radius: var(--radius);
  padding: 5px 8px;
  font: 0.78125rem var(--mono);
}
.highlight-form-check { display: flex; align-items: center; gap: 3px; white-space: nowrap; }
.highlight-form select {
  background: var(--well);
  color: var(--ink-bright);
  border: 1px solid var(--frame-line);
  border-radius: var(--radius);
  padding: 5px 6px;
  font: 0.78125rem var(--sans);
}

/* -------------------------------------------------------- P10.6: log lines
   A gagged line is hidden by the SAME `[hidden]` mechanism the log filter
   already uses — never a second, competing way to hide a line. A highlight
   colour is one of exactly four fixed classes; storyEntry never sees or
   emits anything else (view.js's own comment: never a user-supplied class). */
.log .entry.hl-red, .pane-log .entry.hl-red { color: var(--hurt); }
.log .entry.hl-yellow, .pane-log .entry.hl-yellow { color: var(--death); }
.log .entry.hl-green, .pane-log .entry.hl-green { color: var(--gain); }
.log .entry.hl-blue, .pane-log .entry.hl-blue { color: var(--mana); }

/* A room's title line, in the log itself — a visual separator from
   whatever came before it (leaving a fight, walking into a new place), the
   same "[Name] - Zone" shape `transport/telnetColor.ts` colours on telnet. */
.log .entry.room-title-line {
  margin-top: 8px;
  padding-top: 6px;
  border-top: 1px solid var(--frame-line);
  color: var(--gold);
  font-weight: 700;
}

/* ------------------------------------------------------ P10.6: hotkey bar */
.hotkey-bar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px;
  /* No border/background of its own — sits in the same frame gap `.dock` does; each button carries its own panel colour. */
}
.hotkey { display: inline-flex; align-items: stretch; }
.hotkey-btn {
  background: var(--well);
  color: var(--ink);
  border: 1px solid var(--frame-line);
  border-right: none;
  border-radius: var(--radius) 0 0 var(--radius);
  padding: 4px 10px;
  font: 0.78125rem var(--sans);
  cursor: pointer;
}
.hotkey-btn:hover { border-color: var(--gold-dim); color: var(--gold); }
.hotkey-edit {
  background: var(--well);
  color: var(--ink-dim);
  border: 1px solid var(--frame-line);
  border-right: none;
  border-radius: 0;
  padding: 4px 6px;
  font: 0.78125rem var(--sans);
  cursor: pointer;
  line-height: 1;
}
.hotkey-edit:hover { color: var(--gold); border-color: var(--gold-dim); }
.hotkey-remove {
  background: var(--well);
  color: var(--ink-dim);
  border: 1px solid var(--frame-line);
  border-radius: 0 var(--radius) var(--radius) 0;
  padding: 4px 7px;
  font: 0.78125rem var(--sans);
  cursor: pointer;
  line-height: 1;
}
.hotkey-remove:hover { color: var(--hurt); border-color: var(--hurt); }
.hotkey-add { padding: 4px 10px; }
.hotkey-empty { font-size: 0.78125rem; padding: 4px 0; }

/* ----------------------------------------------------------- responsive */
@media (max-width: 700px) {
  /* Single column, layout editing disabled (BUILD_PLAN P10.4's own
     requirement): the splitters and every per-pane move/reset control
     disappear, and both columns stack into one scrollable strip under the
     log regardless of which side a pane was assigned to on a wider screen
     — column assignment stops mattering below this width, only matters
     again once the window widens back past it (nothing here touches the
     saved layout itself, only how it is painted). */
  .layout { flex-direction: column; }
  .log-wrap { order: 1; flex: 1 1 auto; }
  #splitter-left, #splitter-right, .pane-controls, #reset-layout { display: none; }
  /* `.side-col`/`.side-col-movable` are `display:flex` at wider widths so a
     `flex:0 0 auto` pane (room/map/char) can sit content-sized beside a
     `flex:1 1 0; min-height:0` one (talk/arrivals/deaths) that shares
     whatever height is LEFT OVER — which only means something when the
     column's own height is definite (desktop: stretched to `.layout`'s
     real height by `align-items:stretch`). On mobile `.layout` stacks in a
     column, so a `.side-col`'s own height becomes intrinsic — and a
     `flex-basis:0` item with `min-height:0` inside an intrinsically-sized
     flex container resolves to ZERO rather than its content size (the
     `min-height:auto` safety net that would normally prevent this is
     exactly what `min-height:0` turns off). Switching both to `display:
     block` here sidesteps the whole calculation: every pane just takes its
     own content height in normal block flow, the same fix the original
     pre-P10.4 `.side` used. */
  .side-col, .side-col-movable { display: block; }
  .side-col {
    order: 2;
    width: auto !important;
    max-height: 46vh;
    overflow-y: auto;
  }
  /* Grid row-tracks inside each card depend on its container having a
     definite height to divide up, which a scrolling strip does not. A
     `1fr` track inside an otherwise auto-sized ancestor is ambiguous
     enough in practice that a card's content painted past its own box and
     over the next card, rather than the box growing to fit it. Plain block
     flow has no such ambiguity: each card, and each header-then-body
     inside it, is exactly as tall as its content, one after another,
     guaranteed. */
  .pane, .pane-static { display: block; margin-bottom: var(--gap); }
  .pane:last-child { margin-bottom: 0; }
  .room-body, .pane-log { max-height: 26vh; }

  .log-toolbar { flex-wrap: wrap; }

  /* P10.3: the bottom bar is already exactly two rows (index.html's
     `.sb-row-top`/`.sb-row-bottom`); a narrow window only tightens it —
     nothing here changes row count or DOM order. */
  .statusbar { padding: 5px 8px; gap: 4px; }
  .sb-row { gap: 4px 10px; }
  .sb-vitals { flex-basis: 100%; }
  .sb-target:not(:empty) { flex-basis: 100%; }
  .sb-vitals .bar, .sb-target .bar, .sb-group .bar { height: 16px; min-width: 0; }
  .sb-tick { flex: 1 1 auto; }
}

@media (max-width: 480px) {
  .log { font-size: 0.875rem; padding: 8px 10px; }
  .gate-card { padding: 22px 20px; }
}

/* ======================================================== W1: new panes
   W1_CLIENT_BUILD.md: Quests / Group / Market, click-to-examine, and the
   phone layout's bottom action bar. */
.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}
.quests-panel, .group-panel, .market-panel { padding: 6px 10px; overflow-y: auto; font: 0.8125rem/1.4 var(--sans); min-height: 0; }
.pane-empty { padding: 4px 0; }
.bar-quest .bar-fill { background: var(--xp); }
.quests-panel .bar, .group-panel .bar { height: 16px; }

/* An item name that examines on click: looks like the text it replaced, reads as a button. */
.item-examine {
  font: inherit; color: inherit; background: none; border: 0; padding: 0;
  text-align: left; cursor: pointer; text-decoration: underline dotted; text-underline-offset: 3px;
}
.item-examine:hover { text-decoration-style: solid; }
.item-examine:focus-visible { outline: 2px solid var(--focus); outline-offset: 1px; }

.quest-list, .group-list { list-style: none; margin: 0; padding: 0; display: grid; gap: 8px; }
.quest-bounty { margin-bottom: 8px; display: grid; gap: 3px; }
.quest-row { display: grid; gap: 3px; }
.quest-details summary { cursor: pointer; }
.quest-name { color: var(--ink-bright); }
.quest-done .quest-name { color: var(--gain); }
.quest-desc { margin: 4px 0 2px; color: var(--ink-dim); }
.quest-note { font-size: 0.75rem; color: var(--gold); }
.muted.quest-note { color: var(--ink-dim); }

.group-member { display: grid; gap: 3px; }
.group-member-head { display: flex; justify-content: space-between; gap: 6px; flex-wrap: wrap; }
.group-name { color: var(--ink-bright); }
.group-away { opacity: 0.7; }

.market-table th { text-align: left; font-weight: 600; padding: 2px 4px; }
.market-table td { font-size: 0.78125rem; }

.mobile-bar { display: none; }

@media (max-width: 700px) {
  /* Below ~700px the side columns are gone until a pane is opened from the
     bottom bar; then only that pane shows, as a sheet above the bar. The
     saved desktop layout (columns, order, collapsed) is never touched. */
  .game { grid-template-rows: minmax(0, 1fr) auto auto auto auto; }
  .mobile-bar {
    display: flex; gap: 4px; overflow-x: auto; padding: 2px 0;
    scrollbar-width: none;
  }
  .mobile-btn {
    flex: 0 0 auto; min-height: 40px; min-width: 52px; padding: 6px 10px;
    font: 0.8125rem var(--sans); color: var(--ink); background: var(--panel);
    border: 1px solid var(--frame-line); border-radius: var(--radius); cursor: pointer;
  }
  .mobile-btn.active { color: var(--ink-bright); border-color: var(--gold); background: var(--chip-bg); }
  .mobile-cmd-btn { color: var(--exit); }
  .mobile-btn:focus-visible { outline: 2px solid var(--focus); outline-offset: 1px; }
  body:not([data-mobile-pane]) .side-col { display: none; }
  .side-col .pane, .side-col .pane-static { display: none; }
  .side-col .pane.mobile-open, .side-col .pane-static.mobile-open { display: block; }
  .side-col .mobile-open[data-collapsed] > :not(header) { display: block; }
  .side-col .mobile-open .pane-toggle::before { content: ""; }
  .side-col .mobile-open .room-body, .side-col .mobile-open .pane-log { max-height: 40vh; }
}

/* ======================================================== W2: phone pass
   W2_CLIENT_BUILD.md: answer buttons, the riding chip, and the phone layout —
   44 px touch targets, a 16 px base font, the command line pinned above the
   on-screen keyboard (--vv-top/--vv-height from app.js's visualViewport
   listener), an exits row in place of the compass, the pane bar and the
   quick commands as two thumb rows, and no horizontal scroll at 360 px. */
.chip-riding { border-color: var(--chip-grouped); color: var(--chip-grouped); }

.choice-row { display: flex; flex-wrap: wrap; gap: var(--gap); }
.choice-row[hidden] { display: none; }
.choice-btn,
.gate-card .choice-btn {
  min-height: 44px; min-width: 72px; padding: 6px 18px;
  font: 700 0.9375rem var(--sans); color: var(--ink-bright);
  background: var(--chip-bg); border: 1px solid var(--gold-dim); border-radius: var(--radius);
  cursor: pointer;
}
.choice-btn:hover, .gate-card .choice-btn:hover { border-color: var(--gold); filter: none; }
.choice-btn:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; }

.mobile-exits { display: none; }
.mobile-break { display: none; }

@media (max-width: 700px) {
  /* A readable base on a phone: 16 px (the ui-scale slider still multiplies it). */
  html { font-size: calc(16px * var(--ui-scale)); }
  .log { font-size: 0.9375rem; line-height: 1.45; }
  /* 16 px or more in a focused field, or iOS zooms the whole page in and leaves it there. */
  .prompt input, .prompt .caret, #gate-input, .log-filter-input { font-size: max(16px, 1rem); }

  /* The game fills the VISIBLE box: on iOS the keyboard shrinks only the visual
     viewport, so without this the command line sat under the keyboard. */
  .game {
    position: fixed; left: 0; right: 0;
    top: var(--vv-top, 0px);
    height: var(--vv-height, 100dvh);
    grid-template-rows: minmax(0, 1fr);
    grid-auto-rows: auto;
    padding: 4px max(4px, env(safe-area-inset-right)) max(4px, env(safe-area-inset-bottom)) max(4px, env(safe-area-inset-left));
  }
  .game, .layout, .log-wrap, .log, .dock, .statusbar { min-width: 0; max-width: 100%; }
  .log { overflow-x: hidden; overflow-wrap: anywhere; }

  /* Touch targets: 44 px or more on everything a thumb presses. */
  .toolbar-btn, .jump-bottom, .exit-btn, .room-entity-action, .char-tab, .hotkey-btn,
  .pane-toggle, .gate-card button:not(.creation-card), .exit-row-btn, .mobile-btn {
    min-height: 44px;
  }
  .toolbar-btn, .exit-btn, .room-entity-action, .char-tab, .exit-row-btn, .mobile-btn { min-width: 44px; }
  .room-entity summary, .quest-details summary { min-height: 44px; box-sizing: border-box; }
  .item-examine { min-height: 44px; }
  .market-table .item-examine { min-width: 44px; }
  .prompt input { min-height: 44px; box-sizing: border-box; }
  #gate-input { min-height: 44px; box-sizing: border-box; }

  /* The log toolbar goes: filter, copy, layout and fullscreen are desktop tools, and Settings is on the phone bar. */
  .log-toolbar { display: none; }

  /* The hotkey bar's buttons are the phone bar's quick commands (renderMobileBar), one row, not two. */
  .hotkey-bar { display: none; }

  /* The compass gives way to the exits row: nine 44 px cells would not fit. */
  .sb-compass { display: none; }
  .mobile-exits { display: flex; flex-wrap: wrap; gap: 4px; }
  .mobile-exits:empty { display: none; }
  .exit-row-btn {
    flex: 1 1 0; max-width: 72px; padding: 4px 6px;
    font: 700 0.875rem var(--sans); color: var(--exit);
    background: var(--well); border: 1px solid var(--frame-line); border-radius: var(--radius); cursor: pointer;
  }
  .exit-row-btn.exit-closed { color: var(--ink-dim); border-style: dashed; }
  .exit-row-btn:focus-visible { outline: 2px solid var(--focus); outline-offset: 1px; }

  /* The phone bar: panes on the first row, quick commands on the second — no sideways scrolling. */
  .mobile-bar { flex-wrap: wrap; overflow-x: visible; gap: 4px; }
  .mobile-break { display: block; flex-basis: 100%; height: 0; }
  .mobile-btn { min-height: 44px; min-width: 44px; }
  .mobile-pane-btn { flex: 1 1 0; padding: 4px 2px; font-size: 0.75rem; }
  .mobile-cmd-btn { flex: 1 1 auto; font-size: 0.875rem; }

  /* The pane sheet: art and the map fit the width; the banner is a strip, not a poster. */
  .side-col { max-height: 38vh; max-height: 38dvh; }
  /* In a sheet the pane's own header (collapse toggle) does nothing — the bar's pressed button names the pane. */
  .side-col .mobile-open > header { display: none; }
  .room-map svg { display: block; margin: 0 auto; }

  /* The status bar: vitals as a 2x2 grid so every label reads whole; the tick sweep and an empty group line go. */
  .sb-vitals { display: grid; grid-template-columns: 1fr 1fr; gap: 4px; min-width: 0; }
  .sb-tick, .sb-group .sb-empty { display: none; }
  .mobile-settings-btn { flex: 0 0 44px; font-size: 1.125rem; color: var(--ink); }
  .side-col .mobile-open .room-body, .side-col .mobile-open .pane-log,
  .side-col .mobile-open .char-tab-body { max-height: 34vh; max-height: 34dvh; }
  .room-banner, .room-banner img { max-height: 72px; }
  .room-map, .room-map svg { max-width: 100%; }
  .room-map svg { height: auto; }
  .gate-card.has-cards { width: 100%; }
  .creation-grid { grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); }

  /* The on-screen keyboard is up: keep the log, the vitals and the command line; fold the rest. */
  body[data-keyboard="open"] .sb-row-bottom,
  body[data-keyboard="open"] .mobile-exits,
  body[data-keyboard="open"] .mobile-bar,
  body[data-keyboard="open"] .side-col { display: none; }
}

@media (max-width: 480px) {
  .log { font-size: 0.9375rem; padding: 6px 8px; }
  .gate { padding: 8px; }
  .gate-card { padding: 18px 14px; }
}

/* ======================================================== THE MUSTER M4b: the Training pane
   M4B_CLIENT_BUILD.md: the checklist (current step marked, "Now:" with a
   tap-to-send command), Next / Skip / Re-take, and the lessons; the creation
   card beside M0's Muster Yard / Skip buttons. Colours reuse existing tokens. */
.training-panel { padding: 6px 10px; overflow-y: auto; font: 0.8125rem/1.4 var(--sans); min-height: 0; display: grid; gap: 8px; }
.training-state { margin: 0; color: var(--ink); }
.training-now { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; }
.training-now-label { color: var(--ink-bright); font-weight: 600; }
.training-list, .training-schools, .training-lesson-list { list-style: none; margin: 0; padding: 0; display: grid; gap: 3px; }
.training-step, .training-school, .training-lesson { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
.training-mark { width: 1em; text-align: center; color: var(--ink-dim); }
.training-done .training-mark, .training-done > .training-label { color: var(--gain); }
.training-current { color: var(--ink-bright); font-weight: 600; }
.training-current .training-mark { color: var(--gold); }
.training-school .training-label, .training-lesson .training-label { flex: 1 1 8em; }
.training-head { margin: 4px 0 0; font: 600 0.75rem var(--sans); color: var(--ink-dim); text-transform: uppercase; letter-spacing: 0.04em; }
.training-actions { display: flex; flex-wrap: wrap; gap: 6px; }
.training-btn {
  min-height: 28px; padding: 3px 12px;
  font: 600 0.8125rem var(--sans); color: var(--ink-bright);
  background: var(--chip-bg); border: 1px solid var(--gold-dim); border-radius: var(--radius); cursor: pointer;
}
.training-btn:hover { border-color: var(--gold); }
.training-btn:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; }
.training-cmd { font-family: var(--mono, monospace); color: var(--gold); }
/* PT6: the step's own sentence, under the Now line; the command a walk leads to. */
.training-text { margin: 0; color: var(--ink); }
.training-then { font-family: var(--mono, monospace); }
.training-lessons summary { cursor: pointer; color: var(--ink); }
.training-lessons[open] summary { margin-bottom: 4px; }

.muster-card {
  margin-top: 10px; padding: 10px 12px;
  background: var(--well); border: 1px solid var(--frame-line); border-radius: var(--radius);
}
.muster-card-title { margin: 0 0 4px; font: 700 0.9375rem var(--sans); color: var(--gold); }
.muster-card-line { margin: 0 0 4px; font: 0.8125rem/1.4 var(--sans); color: var(--ink); }

@media (max-width: 700px) {
  .training-btn, .training-lessons summary { min-height: 44px; min-width: 44px; box-sizing: border-box; }
  .training-lessons summary { display: flex; align-items: center; }
  .side-col .mobile-open .training-panel { max-height: 34vh; max-height: 34dvh; }
  /* PT6: compact by default on a phone — the sheet is only as tall as the Now block, so the log keeps its room. */
  body[data-mobile-pane="training"] .side-col:has(#pane-training[data-compact="true"]) { max-height: 26vh; max-height: 26dvh; }
}
@media (max-width: 380px) {
  /* Eight pane buttons do not fit one 44 px row at 360 px: two rows of four. */
  .mobile-bar[data-pane-count="8"] .mobile-pane-btn { flex: 1 1 22%; }
  /* ...and the second row comes out of the sheet, so the log keeps its room. */
  body:has(#mobile-bar[data-pane-count="8"]) .side-col { max-height: 30vh; max-height: 30dvh; }
  body:has(#mobile-bar[data-pane-count="8"]) .side-col .mobile-open .room-body,
  body:has(#mobile-bar[data-pane-count="8"]) .side-col .mobile-open .pane-log,
  body:has(#mobile-bar[data-pane-count="8"]) .side-col .mobile-open .char-tab-body,
  body:has(#mobile-bar[data-pane-count="8"]) .side-col .mobile-open .training-panel { max-height: 26vh; max-height: 26dvh; }
}

/* ======================================================== D2: VELLUM
   D2_DESIGN_BUILD.md. The component layer of the Vellum direction
   (UI_DIRECTION.md "Direction A"): every rule below restyles an existing
   element (or styles one D2 added: the scene, the foe card, the icons, the
   phone's More sheet) through the tokens at the top of this file. It sits last
   on purpose, so the older sections above keep their own documented fixes and
   this block only changes how things look. The W2 and M4b phone rules above
   stay the authority on touch targets and the keyboard fold. */

/* ---- icons: the sprite in index.html, drawn in currentColor ---- */
.ic { width: 1.1em; height: 1.1em; flex: none; fill: none; stroke: currentColor; stroke-width: 1.6; stroke-linecap: round; stroke-linejoin: round; vertical-align: -0.2em; }
.icon-sprite { display: none; }
.span-icon { width: 0.95em; height: 0.95em; margin-right: 0.18em; stroke-width: 2; vertical-align: -0.12em; }
.span-hostile::before, .span-elite::before { content: none; }
.span-elite .span-icon { fill: currentColor; stroke-width: 1.2; }

/* ---- the book face everywhere; figures that line up where numbers are read ---- */
body { font-family: var(--f-book); line-height: 1.45; }
.bar-label, .char-table, .sb-meta, .market-table, .scale-value, .quests-panel .bar, .group-panel .bar { font-variant-numeric: lining-nums tabular-nums; }

/* ---- the gate ---- */
.gate::before {
  background-image:
    linear-gradient(180deg, color-mix(in srgb, var(--bg) 70%, transparent), color-mix(in srgb, var(--bg) 88%, transparent)),
    radial-gradient(ellipse at 50% -10%, color-mix(in srgb, var(--gold) 12%, transparent), transparent 55%),
    url("/art/splash.webp");
}
.gate-card, .settings-dialog {
  border: 1px solid var(--rule-strong);
  box-shadow: inset 0 0 0 4px var(--panel), inset 0 0 0 5px var(--frame-line), var(--shadow-dialog);
}
.gate-card h1 { display: flex; align-items: center; gap: 0.4em; font: 700 1.5rem/1.1 var(--f-caps); color: var(--rubric); letter-spacing: 0.04em; }
.gate-mark { width: 1em; height: 1em; color: var(--gilt-line); stroke-width: 1.4; fill: color-mix(in srgb, var(--gilt-line) 25%, transparent); }
.gate-prompt { font: var(--t-base)/1.5 var(--f-book); }
.gate-lines { font: var(--t-sm)/1.5 var(--f-book); }
.gate-card input, #gate-input { font-family: var(--mono); background: var(--well); border-color: var(--rule-strong); }
.gate-card button:not(.creation-card):not(.choice-btn) { background: var(--rubric); border-color: var(--rubric); color: var(--on-accent); font: 700 var(--t-base) var(--f-caps); letter-spacing: 0.06em; }
.creation-card { border-color: var(--frame-line); background: var(--panel); box-shadow: inset 0 0 0 3px var(--panel), inset 0 0 0 4px var(--frame-line); padding: 10px; }
.creation-card:hover, .creation-card:focus-visible { border-color: var(--rubric); filter: none; }
.creation-card-art { border: 1px solid var(--gilt-line); border-radius: 1px; }
.creation-card-name { font: 700 var(--t-base) var(--f-caps); color: var(--rubric); letter-spacing: 0.03em; }
.creation-card-blurb { font-size: var(--t-xs); }
.muster-card { background: var(--panel); border-color: var(--rule-strong); }
.muster-card-title { font: 700 var(--t-base) var(--f-caps); color: var(--rubric); }

/* ---- controls ---- */
.toolbar-btn, .hotkey-btn, .hotkey-edit, .hotkey-remove, .choice-btn, .gate-card .choice-btn, .training-btn, .exit-btn, .room-entity-action, .foe-flee, .scene-toggle, .zmap-toggle-btn, .zmap-level-btn, .sound-cue-try {
  display: inline-flex; align-items: center; justify-content: center; gap: 0.35em;
  background: var(--panel); color: var(--ink); border: 1px solid var(--rule-strong); border-radius: var(--radius);
  font: 500 var(--t-sm)/1.2 var(--f-caps); letter-spacing: 0.03em;
}
.toolbar-btn .ic, .hotkey-btn .ic, .hk .ic { color: var(--gold); }
.toolbar-btn:hover, .hotkey-btn:hover, .hotkey-edit:hover, .training-btn:hover, .foe-flee:hover, .scene-toggle:hover, .zmap-toggle-btn:hover { border-color: var(--rubric); color: var(--rubric); }
.toolbar-btn[aria-pressed="true"], .zmap-toggle-btn[aria-pressed="true"], .zmap-level-btn[aria-pressed="true"] { border-color: var(--rubric); color: var(--rubric); background: color-mix(in srgb, var(--rubric) 10%, var(--panel)); box-shadow: inset 0 -2px 0 var(--rubric); }
.hotkey-remove:hover { border-color: var(--rubric); color: var(--rubric); }
.hotkey-edit .ic, .hotkey-remove .ic { width: 0.95em; height: 0.95em; }
.choice-btn, .gate-card .choice-btn { font: 700 var(--t-base) var(--f-caps); color: var(--ink-bright); border-color: var(--rubric); background: var(--panel); }
.choice-btn:hover, .gate-card .choice-btn:hover { background: color-mix(in srgb, var(--rubric) 14%, var(--panel)); }
.log-filter-input, .settings-row select, .highlight-form input[type="text"], .highlight-form select { background: var(--well); border-color: var(--rule-strong); color: var(--ink-bright); font-family: var(--f-book); }
.log-filter-input::placeholder { font-style: italic; }
.jump-bottom { background: var(--gold); color: var(--well); border-color: var(--gold); border-radius: var(--radius); font: 700 var(--t-sm) var(--f-caps); letter-spacing: 0.04em; display: inline-flex; align-items: center; gap: 0.3em; }

/* ---- folios: panes, the log, the dock ---- */
.pane, .pane-static, .statusbar, .log {
  border: 1px solid var(--rule-strong);
  border-radius: var(--radius);
  box-shadow: inset 0 0 0 3px var(--panel), inset 0 0 0 4px var(--frame-line);
}
.pane, .pane-static { overflow: hidden; }
.pane-head-row { border-bottom: 1px solid var(--frame-line); margin: 0 6px; padding-right: 0; }
.pane-head, .pane-toggle { display: flex; align-items: center; gap: 0.45em; font: 700 var(--t-sm)/1.2 var(--f-caps); letter-spacing: 0.05em; text-transform: none; color: var(--rubric); padding: 7px 4px; }
.pane-toggle:hover { color: var(--ink-bright); }
.pane-icon { color: var(--gold); width: 1.05em; height: 1.05em; }
.pane-name { overflow: hidden; text-overflow: ellipsis; }
/* The collapse chevron is drawn, not a glyph: a turned corner of the rule. */
.pane-toggle::before { content: ""; order: 3; margin-left: auto; width: 0.42em; height: 0.42em; border-right: 1.5px solid var(--ink-dim); border-bottom: 1.5px solid var(--ink-dim); transform: rotate(45deg) translate(-2px, -2px); flex: none; }
[data-collapsed="true"] .pane-toggle::before { content: ""; transform: rotate(-45deg); }
.pane-toggle { order: 0; }
.pane-controls { gap: 3px; }
.pane-ctrl { display: grid; place-items: center; width: 22px; height: 22px; background: var(--panel); border-color: var(--frame-line); color: var(--ink-dim); border-radius: var(--radius); }
.pane-ctrl .ic { width: 13px; height: 13px; }
.pane-log { font: var(--t-sm)/1.5 var(--f-book); }

/* The right column never overlaps itself (D1 audit finding 1): a pane keeps its own height and the column scrolls,
   rather than panes shrinking under their headers and painting into each other at 800 px tall. Talk, Arrivals and
   Deaths take what is left, with a floor. */
.side-col [data-pane] { flex: 0 0 auto; }
.side-col :where(.pane)[data-pane="talk"], .side-col :where(.pane)[data-pane="arrivals"], .side-col :where(.pane)[data-pane="deaths"] { flex: 1 0 6.5rem; } /* PTV-FIXES2 §6: panes only, same specificity */
.side-col [data-collapsed="true"] { flex: 0 0 auto; }
/* One vitals row: the dock's. The pinned Vitals pane shows only when the bottom bar is switched off (Settings). */
body:not([data-bottom-bar="false"]) #pane-vitals { display: none; }

/* ---- the scene: the room's painting at the head of the log, an inset miniature ---- */
.scene { position: relative; z-index: 2; flex: none; }
.scene-frame { position: relative; height: clamp(7.5rem, 21vh, 11.5rem); padding: 4px; background: var(--panel); border: 1px solid var(--rule-strong); border-radius: var(--radius); box-shadow: inset 0 0 0 1px var(--gilt-line); overflow: hidden; }
.scene-art { width: 100%; height: 100%; }
.scene-art img { display: block; width: 100%; height: 100%; object-fit: cover; object-position: 50% 45%; border: 1px solid var(--gilt-line); }
.scene-caption { position: absolute; left: 16px; bottom: 14px; display: grid; max-width: calc(100% - 96px); padding: 4px 12px 6px; background: var(--panel); border: 1px solid var(--rule-strong); box-shadow: inset 0 0 0 2px var(--panel), inset 0 0 0 3px var(--gilt-line); }
.scene-title { font: 700 var(--t-md)/1.15 var(--f-caps); color: var(--rubric); letter-spacing: 0.03em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.scene-zone { font: italic var(--t-sm)/1.25 var(--f-book); color: var(--ink-dim); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.scene-toggle { position: absolute; top: 10px; right: 10px; width: 34px; height: 34px; padding: 0; z-index: 1; }
.scene-toggle .ic { color: var(--gold); }
.scene[data-folded="true"] .scene-frame { height: auto; padding: 0; box-shadow: none; }
.scene[data-folded="true"] .scene-art { display: none; }
.scene[data-folded="true"] .scene-caption { position: static; display: flex; align-items: baseline; gap: 0.6em; max-width: none; border: 0; box-shadow: none; padding: 7px 52px 7px 12px; }
.scene[data-folded="true"] .scene-toggle { top: 50%; transform: translateY(-50%); width: 30px; height: 30px; }

/* ---- the foe card: only while a fight is real ---- */
.foe-card { position: relative; z-index: 2; flex: none; display: flex; align-items: center; gap: 12px; padding: 8px 12px; background: var(--panel); border: 1px solid var(--rubric); border-radius: var(--radius); box-shadow: inset 0 0 0 3px var(--panel), inset 0 0 0 4px color-mix(in srgb, var(--rubric) 40%, transparent); }
.foe-card:has(#sb-target:empty) { display: none; }
.foe-card .sb-target { flex: 1 1 auto; display: flex; align-items: center; gap: 12px; min-width: 0; }
.foe-card .sb-target .bar { flex: 1 1 auto; height: 1.5rem; min-width: 0; }
.foe-card .foe-portrait { margin: 0; }
.foe-card .foe-portrait .entity-thumb, .foe-card .foe-portrait .entity-thumb-fallback { width: 3.5rem; height: 3.5rem; }
.foe-flee { flex: none; padding: 6px 12px; color: var(--rubric); }
.fx-mount:empty { display: none; }
.fx-ceremony { position: fixed; inset: 0; z-index: 600; pointer-events: none; }

/* ---- portraits: a gilt roundel ---- */
.entity-thumb-mob, .entity-thumb-fallback-mob, .foe-portrait .entity-thumb, .foe-portrait .entity-thumb-fallback {
  border: 2px solid var(--gilt-line); box-shadow: 0 0 0 1px var(--rule-strong);
}
.entity-thumb-fallback { background: var(--well); border-style: solid; }

/* ---- the log: a book page ---- */
.log {
  flex: 1 1 auto;
  background: color-mix(in srgb, var(--panel) var(--log-opacity), transparent);
  padding: 12px 20px;
  font: 400 var(--t-log)/1.6 var(--f-book);
}
.log .entry { margin-bottom: 3px; }
.log .entry.tabular { font: 0.875rem/1.5 var(--mono); }
.log .entry.cmd { font: var(--t-sm)/1.5 var(--mono); color: var(--gold); margin: 12px 0 4px; }
.log .entry.room-title-line { font: 700 var(--t-lg)/1.25 var(--f-caps); color: var(--rubric); letter-spacing: 0.03em; margin-top: 14px; padding-top: 8px; border-top: 1px solid var(--frame-line); }
.log .entry.death { color: var(--rubric); font-family: var(--f-caps); letter-spacing: 0.04em; }
.log .entry.defeat { background: color-mix(in srgb, var(--rubric) 14%, transparent); border-left-color: var(--rubric); font-family: var(--f-caps); }
.log .entry.you { color: var(--ink-bright); }
.span-hostile { color: var(--rubric); }
.span-elite { color: var(--elite); }
.s-system { font-style: italic; }
body[data-timestamps="true"] .log .entry[data-time]::before { font-family: var(--mono); font-size: 0.75em; }
.log-backdrop img { filter: var(--backdrop-filter); }
/* PTV-FIXES §3: in light, a parchment veil over the blurred painting (the log itself is opaque there), so the gaps
   around the log read as clean vellum rather than a grey smear. 0 (no veil) in dark. */
.log-backdrop::after { content: ""; position: absolute; inset: 0; z-index: 1; pointer-events: none; background: var(--panel); opacity: var(--backdrop-veil); }
.log-backdrop img.backdrop-room { filter: var(--backdrop-room-filter); }

/* ---- rarity: the colour and a static glow (decoration; the item's own name and colour carry it) ---- */
.rarity-rare { color: var(--lapis); text-shadow: 0 0 0.55em var(--rarity-glow-rare); }
.rarity-epic { color: var(--s-tell); text-shadow: 0 0 0.6em var(--rarity-glow-epic); }
.rarity-legendary { color: var(--gold); text-shadow: 0 0 0.7em var(--rarity-glow-legendary); }

/* ---- the command line: sunk into the page ---- */
.prompt { background: var(--well); border: 1px solid var(--rule-strong); border-radius: var(--radius); box-shadow: inset 0 2px 6px rgba(0, 0, 0, 0.35); }
.prompt .caret { font: 700 1.05rem var(--f-caps); color: var(--rubric); padding: 0 6px 0 12px; }
.prompt input { font: 0.9375rem var(--mono); color: var(--ink-bright); }
.prompt input::placeholder { color: var(--ink-dim); font-style: italic; }
.prompt:focus-within { outline: 2px solid var(--focus); outline-offset: 1px; }

/* ---- bars: ruled, a tick every tenth ---- */
.bar { background: var(--bar-well); border: 1px solid var(--rule-strong); border-radius: 1px; }
.bar::after { content: ""; position: absolute; inset: 0; pointer-events: none; background: repeating-linear-gradient(90deg, transparent 0 calc(10% - 1px), rgba(0, 0, 0, 0.28) calc(10% - 1px) 10%); }
.bar-label { z-index: 1; font: 600 var(--t-xs)/1 var(--f-book); letter-spacing: 0.01em; text-shadow: 0 1px 1px rgba(0, 0, 0, 0.55); }
.bar-label b { font: 700 0.75rem/1 var(--f-caps); letter-spacing: 0.06em; }
/* PTV-FIXES §2: a long name ("Vitrarch, the Glass Tyrant" on a phone) is cut at its END with an ellipsis, and the
   numbers after it always show. Before, the centred flex row overflowed both ways and clipped the name's start. */
.bar-label { justify-content: safe center; }
.bar-label b { flex: 0 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
.bar-rt .bar-fill { background: var(--gold); }

/* ---- the dock (bottom bar): two rows beside one compact compass ---- */
.statusbar { display: grid; grid-template-columns: minmax(0, 1fr) auto; grid-template-rows: auto auto; align-items: center; column-gap: 18px; row-gap: 6px; padding: 8px 12px; font: var(--t-sm) var(--f-book); background: var(--panel); }
.sb-row-top { grid-column: 1; grid-row: 1; }
.sb-row-bottom { grid-column: 1; grid-row: 2; }
.sb-compass-wrap { grid-column: 2; grid-row: 1 / span 2; gap: 12px; }
.sb-vitals .bar, .sb-group .bar, .sb-rt .bar { height: 1.25rem; }
.sb-tick-track { background: var(--bar-well); height: 3px; }
.sb-clock { font: italic var(--t-xs) var(--f-book); }
.chip { background: none; border: 0; border-bottom: 1px solid currentColor; border-radius: 0; padding: 0 2px 1px; font: italic var(--t-sm)/1.3 var(--f-book); }
.chip-combat { font-style: normal; font-weight: 700; }
.sb-compass { display: flex; align-items: center; gap: 4px; }
.compass-grid { grid-template-columns: repeat(3, 1.375rem); grid-template-rows: repeat(3, 1.375rem); gap: 2px; }
.compass-vertical { gap: 2px; }
.compass-dir { width: 1.375rem; height: 1.375rem; border-radius: 50%; font: 700 0.5625rem/1 var(--f-caps); background: var(--panel); border-color: var(--frame-line); }
.compass-dir.compass-lit { color: var(--verd); border-color: var(--verd); background: color-mix(in srgb, var(--verd) 10%, var(--panel)); }
.compass-dir.compass-lit:hover { background: color-mix(in srgb, var(--verd) 22%, var(--panel)); }
.compass-center { display: grid; place-items: center; }
.compass-center::after { content: ""; width: 6px; height: 6px; border-radius: 50%; background: var(--rubric); }
.sb-meta { font-size: var(--t-sm); gap: 2px; text-align: right; }
.sb-level { font: 700 var(--t-sm) var(--f-caps); color: var(--gold); letter-spacing: 0.03em; }

/* ---- hotkeys ---- */
.hotkey-btn { border-right: none; border-radius: var(--radius) 0 0 var(--radius); padding: 4px 12px; }
.hotkey-edit { border-right: none; border-radius: 0; color: var(--ink-dim); padding: 4px 7px; }
.hotkey-remove { border-radius: 0 var(--radius) var(--radius) 0; color: var(--ink-dim); padding: 4px 7px; }

/* ---- the room pane ---- */
.room-body { font: var(--t-sm)/1.5 var(--f-book); }
.room-title { font: 700 var(--t-base)/1.25 var(--f-caps); color: var(--rubric); letter-spacing: 0.02em; }
.exit-btn { color: var(--verd); font: italic var(--t-sm) var(--f-book); padding: 3px 10px; }
.exit-btn:hover { border-color: var(--verd); background: color-mix(in srgb, var(--verd) 12%, var(--panel)); }
.room-section-label, .training-head { font: 700 var(--t-xs) var(--f-caps); color: var(--gold); text-transform: none; letter-spacing: 0.05em; }
.room-entity { background: var(--panel); border-color: var(--frame-line); }
.room-entity summary { font: var(--t-sm) var(--f-book); }
.room-entity summary::before { content: ""; width: 0.38em; height: 0.38em; border-right: 1.5px solid var(--ink-dim); border-bottom: 1.5px solid var(--ink-dim); transform: rotate(-45deg); margin: 0 0.3em 0 0.1em; flex: none; }
.room-entity[open] summary::before { content: ""; transform: rotate(45deg); }
.room-entity-action { color: var(--verd); }

/* ---- the character pane ---- */
.char-tabs { border-bottom-color: var(--frame-line); margin: 0 6px; }
.char-tab { font: 700 var(--t-xs) var(--f-caps); letter-spacing: 0.04em; text-transform: none; }
.char-tab.active { color: var(--rubric); border-bottom-color: var(--rubric); }
.char-tab-body { font: var(--t-sm)/1.45 var(--f-book); }

/* ---- the map: an inked plan on a hatched ground ---- */
.room-map { background: var(--well) repeating-linear-gradient(45deg, transparent 0 7px, color-mix(in srgb, var(--frame-line) 55%, transparent) 7px 8px); margin: 6px 10px 10px; border: 1px solid var(--frame-line); }
.map-room-visited { fill: var(--panel); stroke: var(--ink); }
.map-room-known { stroke: var(--ink-dim); }
.map-room-player { fill: var(--rubric); stroke: var(--gilt-line); stroke-width: 2; }
.map-link { stroke: var(--ink-dim); stroke-width: 1.4; stroke-dasharray: 1 3.5; stroke-linecap: round; }
.map-player-glow { stroke: var(--gilt-line); }
.map-hostile-dot { fill: var(--rubric); }
/* v-modules' zone map, in the same hand (zoneMap.css reads these --zmap-* tokens). */
#pane-map {
  --zmap-bg: var(--well);
  --zmap-frame: var(--rule-strong);
  --zmap-link: var(--ink-dim);
  --zmap-stub: var(--ink-dim);
  --zmap-hop: var(--ink-dim);
  --zmap-portal: var(--elite);
  --zmap-known: var(--ink-dim);
  --zmap-room: var(--panel);
  --zmap-room-edge: var(--ink);
  --zmap-you: var(--rubric);
  --zmap-you-edge: var(--gilt-line);
  --zmap-door: var(--rubric);
  --zmap-door-other: var(--ink);
  --zmap-up: var(--verd);
  --zmap-down: var(--rubric);
  --zmap-accent: var(--rubric);
  --zmap-accent-ink: var(--ink-bright);
}
.zmap-toggle { padding: 8px 10px 0; }
.zmap-viewport { background: var(--well) repeating-linear-gradient(45deg, transparent 0 7px, color-mix(in srgb, var(--frame-line) 55%, transparent) 7px 8px); border-radius: var(--radius); }
.zmap-empty, .zmap-list { font-family: var(--f-book); }

/* ---- the Training pane ---- */
.training-panel { font: var(--t-sm)/1.45 var(--f-book); }
.training-state { color: var(--ink-dim); font-style: italic; }
.training-progress { height: 6px; background: var(--well); border: 1px solid var(--frame-line); border-radius: 1px; overflow: hidden; }
.training-progress-fill { height: 100%; background: var(--verd); }
.training-now-label { font: 700 var(--t-base)/1.2 var(--f-caps); color: var(--ink-bright); letter-spacing: 0.02em; }
.training-btn { font: 500 var(--t-sm) var(--f-caps); color: var(--ink); background: var(--panel); border-color: var(--rule-strong); }
.training-btn.training-cmd { font: 600 var(--t-sm) var(--mono); background: var(--rubric); border-color: var(--rubric); color: var(--on-accent); }
.training-btn.training-cmd:hover { color: var(--on-accent); filter: brightness(1.08); }
.training-mark { display: inline-grid; place-items: center; width: 1.1em; }
.training-mark .ic { width: 1em; height: 1em; }
.training-step:not(.training-done) .training-mark .ic { fill: currentColor; }
.training-done .training-mark, .training-done > .training-label { color: var(--verd); }
.training-current .training-mark { color: var(--rubric); }
.training-text { color: var(--ink); }
/* The toggle and the Next/Skip row share a line; everything else is full width. */
.training-panel { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
.training-panel > * { flex: 1 1 100%; }
.training-panel > .training-toggle, .training-panel > .training-actions { flex: 0 1 auto; }
/* On a desktop the pane keeps its own scroll, so the Room and Map panes below it stay in the column at 800 px. */
@media (min-width: 701px) {
  .side-col .training-panel { max-height: 30vh; }
  .side-col .room-body { max-height: 22vh; }
}
.training-then { color: var(--ink-dim); font-size: var(--t-xs); }

/* ---- settings ---- */
.settings-title { font: 700 var(--t-md) var(--f-caps); color: var(--rubric); letter-spacing: 0.03em; }
.settings-subtitle { font: 700 var(--t-sm) var(--f-caps); color: var(--rubric); letter-spacing: 0.03em; }
.settings-row { font: var(--t-sm) var(--f-book); }
.settings-fx:empty { display: none; }
.settings-fx { display: grid; gap: 12px; }
.settings-sound { display: grid; gap: 8px; } /* POST-CLIENT: the one Sound section (index.html) */
.settings-dialog::backdrop { background: color-mix(in srgb, var(--bg) 70%, transparent); }

/* ---- motion: tokens, and the Effects kill switch (see the header's MOTION RULES) ---- */
@media (prefers-reduced-motion: no-preference) {
  .scene-art img.art-fade { animation-duration: var(--dur-calm); }
  .toolbar-btn, .pane-ctrl, .training-btn, .exit-btn, .mobile-btn, .compass-dir { transition: color var(--dur-quick) ease, border-color var(--dur-quick) ease, background-color var(--dur-quick) ease; }
  .mobile-more:not([hidden]) { animation: d2-sheet-in var(--dur-quick) ease-out; }
  body[data-mobile-pane] .side-col:has(.mobile-open) { animation: d2-sheet-in var(--dur-quick) ease-out; }
}
@keyframes d2-sheet-in {
  from { opacity: 0; transform: translateY(6px); }
  to { opacity: 1; transform: none; }
}
body[data-effects="off"] *, body[data-effects="off"] *::before, body[data-effects="off"] *::after { animation: none !important; transition: none !important; }

/* ---- the phone (under 700 px): the log keeps at least ~45% of the screen; sheets open OVER it ---- */
@media (max-width: 700px) {
  .game { gap: 4px; }
  .log { font-size: var(--t-log); line-height: 1.5; padding: 8px 12px; }
  .prompt input, #gate-input { font-size: max(16px, 1rem); } /* W2's iOS rule, restated over this block's own font */
  .scene-frame { height: 76px; padding: 3px; }
  .scene-caption { left: 10px; bottom: 8px; padding: 2px 9px 3px; max-width: calc(100% - 72px); }
  .scene-title { font-size: var(--t-base); }
  .scene-zone { display: none; }
  .scene-toggle { top: 50%; transform: translateY(-50%); right: 6px; width: 44px; height: 44px; }
  .scene[data-folded="true"] .scene-toggle { width: 44px; height: 44px; }
  .scene[data-folded="true"] .scene-caption { padding: 10px 56px 10px 12px; }
  .side-col .mobile-open > #training-panel { display: flex; } /* W1's sheet rule set it to block */
  .foe-card { padding: 5px 8px; gap: 8px; }
  .foe-card .sb-target { gap: 8px; }
  .foe-card .foe-portrait .entity-thumb, .foe-card .foe-portrait .entity-thumb-fallback { width: 2.5rem; height: 2.5rem; }
  .foe-flee { min-height: 44px; min-width: 44px; padding: 4px 10px; }
  /* Sheets: laid over the top of the log (the newest lines, at its foot, stay in view), never pushing it up. */
  .layout { position: relative; }
  .side-col { position: absolute; top: 0; left: 0; right: 0; z-index: 8; background: var(--panel); border: 1px solid var(--rule-strong); border-radius: var(--radius); box-shadow: 0 14px 28px -12px rgba(0, 0, 0, 0.6); }
  .side-col:not(:has(.mobile-open)) { display: none; }
  .side-col .pane, .side-col .pane-static { box-shadow: none; border: 0; }
  /* The status bar: the four vitals as a 2x2 block, then one line of chips beside level and coins. */
  .statusbar { grid-template-columns: minmax(0, 1fr) auto; padding: 5px 8px; row-gap: 4px; column-gap: 10px; }
  .sb-row-top { grid-column: 1 / -1; }
  .sb-row-bottom { grid-column: 1; grid-row: 2; flex-wrap: wrap; max-height: 1.75rem; overflow: hidden; }
  .sb-compass-wrap { grid-column: 2; grid-row: 2; }
  .sb-meta { flex-direction: row; gap: 8px; align-items: baseline; }
  .sb-vitals .bar { height: 1.125rem; }
  .exit-row-btn { background: var(--panel); border-color: var(--rule-strong); color: var(--verd); font: 700 var(--t-sm)/1.1 var(--f-caps); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .sb-compass { display: none; }
  .log .entry.room-title-line { font-size: 1.125rem; }
  /* The bar: one row of icons (Train, Room, Map, Char, Quests, Talk, More); More opens the rest as a small sheet. */
  .mobile-bar { display: grid; grid-auto-flow: column; grid-auto-columns: minmax(0, 1fr); gap: 4px; position: relative; padding: 0; }
  .mobile-bar .mobile-btn { min-width: 0; }
  .mobile-pane-btn, .mobile-more-btn { display: grid; justify-items: center; align-content: center; gap: 1px; min-height: 48px; padding: 3px 1px; background: var(--panel); border: 1px solid var(--frame-line); border-radius: var(--radius); color: var(--ink-dim); font: 500 0.6875rem/1.1 var(--f-caps); letter-spacing: 0.03em; }
  .mobile-pane-btn .ic, .mobile-more-btn .ic { width: 1.35rem; height: 1.35rem; }
  .mobile-more-btn .ic { stroke-width: 3.2; }
  .mobile-btn.active, .mobile-more-btn[aria-expanded="true"] { color: var(--rubric); border-color: var(--rule-strong); background: color-mix(in srgb, var(--rubric) 8%, var(--panel)); box-shadow: inset 0 -2px 0 var(--rubric); }
  .mobile-more { position: absolute; left: 0; right: 0; bottom: calc(100% + 6px); z-index: 20; display: grid; grid-template-columns: repeat(auto-fill, minmax(84px, 1fr)); gap: 6px; padding: 10px; background: var(--panel); border: 1px solid var(--rule-strong); border-radius: var(--radius); box-shadow: inset 0 0 0 3px var(--panel), inset 0 0 0 4px var(--frame-line), 0 -10px 24px -10px rgba(0, 0, 0, 0.55); }
  .mobile-more .mobile-btn { min-height: 44px; display: inline-flex; align-items: center; justify-content: center; gap: 0.35em; font: 500 var(--t-sm) var(--f-caps); background: var(--panel); border: 1px solid var(--rule-strong); color: var(--ink); }
  .mobile-more .mobile-pane-btn { grid-auto-flow: column; }
  .mobile-more .mobile-cmd-btn { color: var(--verd); }
  .mobile-more .mobile-settings-btn .ic { width: 1.2rem; height: 1.2rem; }
}

/* ---- SH13 (SH13_MAP_BUILD.md): what a room is, on the Nearby and Zone maps ----
   An icon from the sprite (`i-k-*`) in a visited room's square, in a Vellum token per kind — the same families as
   telnet's map glyphs: rubric for threat, gilt for coin and work, verdigris for care and rest, lapis for learning and
   water, the elite violet for a boss and a way through, ink for a road out. Every one of these tokens is checked at
   4.5:1 on the square (--panel) and the ground (--well) in both themes: tools/client/contrast.mjs MAP_ICON_TOKENS,
   which must name the same token as the rule here (clientD2.test.js / sh13Map.test.js read both). */
.room-map { flex-direction: column; align-items: center; }
/* The Nearby map drawn at up to twice its 116 px: the icons read at a glance, and it still fits any pane or phone. */
.room-map .map-svg { width: min(100%, 13.5rem); height: auto; }
.map-kind, .zmap-kind { fill: none; stroke: currentColor; stroke-width: 2.4; stroke-linecap: round; stroke-linejoin: round; pointer-events: none; }
.map-kind-boss { color: var(--elite); }
.map-kind-danger { color: var(--rubric); }
.map-kind-healer { color: var(--verd); }
.map-kind-trainer { color: var(--lapis); }
.map-kind-shop { color: var(--gold); }
.map-kind-stable { color: var(--verd); }
.map-kind-quest { color: var(--gold); }
.map-kind-inn { color: var(--verd); }
.map-kind-water { color: var(--lapis); }
.map-kind-road { color: var(--ink); }
.map-kind-portal { color: var(--elite); }
.map-kind-locked { color: var(--rubric); }
.map-lock { stroke: var(--rubric); stroke-width: 2.5; stroke-linecap: round; }
.map-legend { list-style: none; margin: 6px 0 0; padding: 3px 8px; display: flex; flex-wrap: wrap; justify-content: center; gap: 2px 12px;
  background: var(--panel); color: var(--ink); border: 1px solid var(--frame-line); border-radius: var(--radius); max-width: 100%; box-sizing: border-box;
  font: var(--t-xs)/1.4 var(--f-book); }
.map-legend li { display: inline-flex; align-items: center; gap: 4px; white-space: nowrap; }
.map-legend .ic { width: 1.05em; height: 1.05em; stroke-width: 2.2; vertical-align: 0; }
.map-legend-you { display: inline-block; width: 0.8em; height: 0.8em; background: var(--rubric); border: 1px solid var(--gilt-line); border-radius: 2px; }
.zmap-legend { margin: 6px 0 0; justify-content: flex-start; }
