/* ============================================================
   Data Dungeon — design tokens + global rules
   ============================================================ */

/* ============================================================
   @font-face — self-hosted Rajdhani + Share Tech Mono
   ------------------------------------------------------------
   Self-hosted (static/fonts/) instead of Google Fonts. Why:
     - cold-load: browser preloads the woff2 in parallel with
       HTML parse, so by the time CSS applies, the font is in
       cache. display: optional therefore picks Rajdhani on
       first paint instead of locking the fallback.
     - cached-load: still instant, no swap reflow on tab-switch
       full-page navs (the symptom commit 7660249 was fixing).
     - no third-party DNS/connect/cert handshake on critical path.

   font-display: optional preserves the prior "no late swap"
   behaviour. The preload link in templates/base.html ensures
   the font arrives within the ~100ms block window so 'optional'
   resolves to "use Rajdhani" instead of "lock fallback."

   OFL-1.1 licensed. See static/fonts/OFL.txt for the license +
   per-font copyright notices. Files are Latin-subset only — the
   only subset the app uses.
   ============================================================ */
@font-face {
  font-family: 'Rajdhani';
  font-style: normal;
  font-weight: 400;
  font-display: optional;
  src: url('../fonts/Rajdhani-400.woff2') format('woff2');
}
@font-face {
  font-family: 'Rajdhani';
  font-style: normal;
  font-weight: 500;
  font-display: optional;
  src: url('../fonts/Rajdhani-500.woff2') format('woff2');
}
@font-face {
  font-family: 'Rajdhani';
  font-style: normal;
  font-weight: 600;
  font-display: optional;
  src: url('../fonts/Rajdhani-600.woff2') format('woff2');
}
@font-face {
  font-family: 'Rajdhani';
  font-style: normal;
  font-weight: 700;
  font-display: optional;
  src: url('../fonts/Rajdhani-700.woff2') format('woff2');
}
@font-face {
  font-family: 'Share Tech Mono';
  font-style: normal;
  font-weight: 400;
  font-display: optional;
  src: url('../fonts/ShareTechMono-400.woff2') format('woff2');
}

/* ============================================================
   CROSS-DOCUMENT NAVIGATION  (was: View Transitions; see LM-17)
   ------------------------------------------------------------
   View Transitions API was enabled here but produced more pain
   than it solved across this app's surfaces:
     - Cross-fade midpoint canvas bleed → bright white flash on
       every nav (Chromium did not respect a dark backdrop on
       ::view-transition for the bleed window).
     - Unmatched named-element fade (e.g. dd-top-nav and
       dd-chip-strip exist on sheet pages but not on home /
       stats / settings) → 250ms position-jitter on entry/exit.
     - Snapshot-vs-live-DOM position mismatch on stats and
       inventory pages → visible content shift on nav.
     - Cumulative snapshot-capture overhead made nav feel slower
       than plain MPA.

   Removing the @view-transition rule disables the entire
   pseudo-tree. Chrome falls back to its native MPA paint-holding
   behavior (default since Chrome 96): the outgoing page stays
   painted until the incoming page is ready, then snap-swaps.
   Persistent chrome (header, logo, nav bars, chip strip) renders
   on each new page but at the same position with the same
   styling, so no perceptible flicker.

   The view-transition-name declarations on .app-header /
   .app-logo / .header-right / .app-nav / .top-nav / .bottom-nav
   / .chip-strip have also been removed since they're no-ops
   without @view-transition enabled. If we ever re-enable VT,
   re-add both blocks together — they only function as a pair.

   See docs/cerebro_misses.yaml entry dated 2026-05-04 for the
   full diagnosis trail. LM-17 doc kept for historical context.
   ============================================================ */

/* ============================================================
   PERSISTENT-SHELL SWAP CONTAINER
   ------------------------------------------------------------
   #dd-shell-content wraps the character-sheet swap region (the
   state/tab banners + <main class="tab-content">) so shell_nav.js
   can replace ONLY that region on a tab nav while the chrome stays
   mounted (the WebKit/iOS fix for the Chrome-only Speculation-Rules
   prefetch — see static/js/shell_nav.js + base.html:355).

   `display: contents` removes the wrapper from the BOX tree, so its
   children (the banners + <main>) lay out EXACTLY as if the wrapper
   weren't there — provably zero layout change vs. the pre-shell
   markup. Selector matching is unaffected too: every .tab-content
   rule uses `.tab-content > …` (targeting <main>'s children), never
   `… > .tab-content`, so the extra DOM level never breaks a combinator.
   ============================================================ */
#dd-shell-content { display: contents; }

/* ============================================================
   SAFE-AREA + VIEWPORT FRAMEWORK
   ------------------------------------------------------------
   Single source of truth for layouts that must respect the
   device's notch / home indicator / dynamic browser toolbar.
   New modals and full-screen layouts MUST follow these rules
   or they will clip on iPhone X+ / iOS Safari with the URL
   bar visible / PWA standalone mode.

   Rules:
   1. Use 100dvh (dynamic viewport height) not 100vh anywhere
      a max-height could clip a modal. 100vh is the LARGEST the
      viewport can be; iOS Safari with toolbars visible can be
      ~75 px shorter than that. Fall back via @supports.
   2. Any element pinned to the bottom of the viewport MUST
      add padding-bottom: env(safe-area-inset-bottom) so the
      home indicator on iPhone X+ doesn't overlap content.
   3. Modal overlays must use the .dd-modal-overlay base — it
      provides safe-area-aware padding and dvh scrolling. Card
      content uses the .dd-modal-card flex layout (sticky head,
      scrollable body, sticky foot).
   ============================================================ */
:root {
  /* Resolved safe-area edges with explicit fallbacks.
     env() returns 0 on browsers without notches, so non-mobile
     desktop renders these as no-op zero. */
  --dd-safe-top:    env(safe-area-inset-top, 0px);
  --dd-safe-bottom: env(safe-area-inset-bottom, 0px);
  --dd-safe-left:   env(safe-area-inset-left, 0px);
  --dd-safe-right:  env(safe-area-inset-right, 0px);

  /* Heights of the persistent app chrome — referenced by sticky
     positioning + modal max-heights so nothing has to recompute. */
  --dd-bottom-nav-h: calc(64px + var(--dd-safe-bottom));
  --dd-modal-pad:    24px;

  /* Dynamic viewport height with safe fallback. dvh accounts for
     iOS Safari's collapsing toolbar; vh is the larger value used
     when dvh isn't supported. */
  --dd-vh: 100vh;
}
@supports (height: 100dvh) {
  :root { --dd-vh: 100dvh; }
}

/* Reusable modal pattern — opt in by adding .dd-modal-overlay /
   .dd-modal-card to your modal markup, OR mirror these rules in
   the modal-specific class. */
.dd-modal-overlay {
  position: fixed;
  inset: 0;
  z-index: 320;
  display: none;
  align-items: center;
  justify-content: center;
  background: var(--overlay-modal);
  /* Safe-area-aware padding so the card never bottoms out into
     the home indicator on iPhone X+. */
  padding:
    calc(var(--dd-modal-pad) + var(--dd-safe-top))
    calc(var(--dd-modal-pad) + var(--dd-safe-right))
    calc(var(--dd-modal-pad) + var(--dd-safe-bottom))
    calc(var(--dd-modal-pad) + var(--dd-safe-left));
  -webkit-overflow-scrolling: touch;
  overflow-y: auto;
}
.dd-modal-overlay.open { display: flex; }

.dd-modal-card {
  width: 100%;
  max-width: 520px;
  /* Cap by dvh minus the overlay padding (×2 because top + bottom). */
  max-height: calc(var(--dd-vh) - 2 * var(--dd-modal-pad)
                   - var(--dd-safe-top) - var(--dd-safe-bottom));
  background: var(--black-2);
  border: 1px solid var(--red-border);
  border-radius: var(--radius);
  display: flex;
  flex-direction: column;
  overflow: hidden;
}
.dd-modal-card > .dd-modal-head { flex: 0 0 auto; }
.dd-modal-card > .dd-modal-body { flex: 1 1 auto; overflow-y: auto; min-height: 0; -webkit-overflow-scrolling: touch; }
.dd-modal-card > .dd-modal-foot { flex: 0 0 auto; }

/* Colour tokens (chrome + accent family + per-theme overrides)
   live in static/css/colors.css and are linked from base.html
   BEFORE this file. See colors.css for the full vocabulary. */
:root {
  /* ── Font standards ──────────────────────────────────────────────
     Two faces for the whole app:
       --font-display: Rajdhani — the 'DATA DUNGEON' font. Also our
         body font. Everything that isn't a stat number uses this.
       --font-mono:    Share Tech Mono — tabular numerals only
         (dice results, HP current/max, ability scores, coin amounts).
     --font-body aliases --font-display for backward compat with rules
     that were pointing at Inter; do not reintroduce a third face. */
  --font-display: 'Rajdhani', sans-serif;
  --font-mono:    'Share Tech Mono', monospace;
  --font-body:    var(--font-display);

  --radius-sm: 3px;
  --radius:    6px;
  --radius-lg: 10px;

  --transition: 0.18s ease;
}

* {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
  -webkit-tap-highlight-color: transparent;
  -webkit-touch-callout: none;    /* suppress long-press menu on text */
  -webkit-text-size-adjust: 100%; /* iOS Safari: no auto-upscaling of text */
  /* Firefox scrollbar default: thin track, grey-dark thumb on a
     transparent rail. Keeps scrollbars subtle on every overflowing
     element without any per-rule styling. Components that
     explicitly hide their scrollbar (.chip-strip, .resource-bar,
     .h-scroll, etc.) override with scrollbar-width: none. */
  scrollbar-width: thin;
  scrollbar-color: var(--grey-dark) transparent;
}

/* Webkit (Chrome / Safari / Edge) scrollbars — match the Firefox
   treatment above. Narrow, rounded, grey-dark thumb with a
   transparent track so the scrollbar doesn't paint a white slab
   against the dark theme. Components that want no scrollbar at all
   set ::-webkit-scrollbar { display: none } with higher specificity. */
::-webkit-scrollbar {
  width: 10px;
  height: 10px;
}
::-webkit-scrollbar-track {
  background: transparent;
}
::-webkit-scrollbar-thumb {
  background-color: var(--grey-dark);
  border-radius: 6px;
  /* 2px transparent border + background-clip:content-box gives the
     thumb a bit of visual padding on either side of its 10px track. */
  border: 2px solid transparent;
  background-clip: content-box;
}
::-webkit-scrollbar-thumb:hover {
  background-color: var(--grey-mid);
  background-clip: content-box;
}
::-webkit-scrollbar-corner {
  background: transparent;
}

/* Textarea resize handle — default Chrome renders a bright accent
   diagonal stripe in the bottom-right corner of resizable textareas
   that clashed hard against the dark theme (user-flagged on the Bio
   tab). Repaint with a subtle grey-mid double-stripe on a black-2
   square so it reads as a proper drag grip inside the app's palette.
   Firefox has no equivalent pseudo-element — its native grip is
   already small and neutral, so no change needed there. */
::-webkit-resizer {
  background-color: var(--black-2);
  background-image:
    linear-gradient(135deg,
      transparent 0%, transparent 30%,
      var(--grey-mid) 30%, var(--grey-mid) 40%,
      transparent 40%, transparent 60%,
      var(--grey-mid) 60%, var(--grey-mid) 70%,
      transparent 70%, transparent 100%);
  border-bottom-right-radius: var(--radius-sm);
}

html, body {
  background: var(--black);
  color: var(--white);
  font-family: var(--font-body);
  /* Rajdhani 400 reads slightly thin at body size next to Inter's
     former 400; bumping the default to 500 restores the weight
     perceptually. Headings/labels still override up to 600/700. */
  font-weight: 500;
  font-size: 15px;
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
  min-height: 100vh;
  /* Lock page to single scale — prevents pinch + double-tap zoom */
  touch-action: pan-x pan-y;
  overscroll-behavior-y: none;    /* prevent bounce-reveal of browser chrome */
  /* Tell the browser the page is dark-themed so native form popups
     (<select> dropdown, date/time pickers, scrollbars, autofill
     highlight) render with dark chrome instead of the light default
     that clashed with the app theme. Accent-color themes the
     remaining system widgets (checkboxes, radios, progress, range)
     in the current theme's red. */
  color-scheme: dark;
  accent-color: var(--red);
}

/* UA stylesheets reset font-family on form controls to system-ui
   regardless of inherited body font. Re-pin every form-element
   class to the project's body font so Rajdhani survives the UA
   reset. Class rules with explicit font-family (e.g. .btn ->
   var(--font-display)) still win on specificity, so this only
   covers form controls whose class list doesn't already pin
   typography. Closes the form_element_font_family scanner class
   for every <button>/<input>/<select>/<textarea>/<summary> on
   the canvas at once.

   See: docs/conventions/cerebro_self_coverage.md →
   "Form-element font-family required" + cerebro_patterns.PATTERNS
   ['form_element_typography']. */
button, input, select, textarea, summary {
  font-family: var(--font-body);
}

/* iOS PWA standalone viewport fix. Without these, `position: fixed;
   bottom: 0` elements (the bottom-nav) drift with scroll because iOS
   miscomputes the visible viewport height in standalone mode when
   `viewport-fit=cover` + `black-translucent` status bar are combined
   — which we use. `-webkit-fill-available` forces iOS to report the
   real standalone viewport so the fixed nav pins correctly. Other
   engines ignore the value (invalid in non-WebKit), so desktop and
   Android are unaffected. */
html { height: -webkit-fill-available; }
body { min-height: -webkit-fill-available; }

/* Reserve space for the vertical scrollbar always, even on pages that
   don't need it. Without this, navigating from a short page (no
   scrollbar) to a long page (scrollbar appears) shrinks the usable
   viewport by ~10-17px depending on the OS, and centered chrome
   (margin:0 auto, max-width:1280px) re-centers around the reduced
   width — every nav between short ↔ long pages produces a 5-9px
   horizontal shift of header / nav bars / content.

   `scrollbar-gutter: stable` is the modern fix (Chromium 94+, Firefox
   97+, Safari 18+). On older browsers the property is silently
   ignored and the shift returns; we accept that since the supported
   browser floor doesn't include those.

   See `visual_layout/scrollbar_gutter_shift` in
   services/cerebro_categories.py and the matching scanner in
   services/graph_violations.py. */
html { scrollbar-gutter: stable; }

/* ---- Text selection policy ----
   Display-only chrome (headings, labels, cards, footer text, etc.)
   does NOT highlight on drag — selection is reserved for elements
   where the user is genuinely entering or copying content (inputs,
   textareas, contenteditables, selects). The site-wide default of
   "everything is selectable" reads as unpolished — dragging across
   a section title or character-card name lights it up blue, which
   breaks the iPhone-of-D&D-tools feel the product bar requires.

   Three layers:
     - Global rule on body — turns selection OFF everywhere by
       default. -webkit-user-select pairs with user-select for
       Safari + iOS WebKit which still ship the prefixed property.
     - Override on form-element selectors — input / textarea / select /
       contenteditable get selection back so users can edit values,
       triple-click to select-all-in-field, copy from a textarea.
     - Inline `<code>` and `<pre>` blocks remain selectable so
       users can copy commands / IDs / SHAs from documentation
       surfaces (DevLog, dev panels, etc.).

   Adding a new element class that the user must be able to select
   text from? Add it to the override selector below with a comment
   explaining the case. The default is "non-selectable"; selectability
   is the opt-in. */
body {
  -webkit-user-select: none;
  -ms-user-select: none;
  user-select: none;
}
input, textarea, select, [contenteditable="true"], code, pre {
  -webkit-user-select: text;
  -ms-user-select: text;
  user-select: text;
}

/* Interactive elements use `manipulation` for zero tap-delay + no zoom */
button, a, input, select, textarea,
.tappable, .bubble, .skill-row, .data-row, .skill-dot, .prof-dot,
.filter-chip, .condition-badge, .tab-link, .nav-item,
.vital, .stat-card, .hp-btn, .resource-chip, .spell-card-head, .do-it,
.roll-pill, .btn {
  touch-action: manipulation;
}

a { color: var(--red-bright); text-decoration: none; }
a:hover { color: var(--white); }

/* ---- Utility ---- */
.red { color: var(--red-bright); }
.grey { color: var(--grey-text); }
.mono { font-family: var(--font-mono); }
.hide { display: none !important; }

/* Prevent scroll-chaining (a.k.a. "background scrolls after the modal
   hits its bottom") on every overlay / dropdown / modal-body we own.
   overscroll-behavior: contain stops the momentum scroll from
   bubbling to ancestors; `touch-action: pan-y` on iOS reinforces it
   for touch gestures. Applied centrally so future overlays just need
   to reuse one of these classes. */
.dd-modal-overlay,
.dd-modal-card,
.dd-modal-card > .dd-modal-body,
.modal-body,
.srd-results,
.roll-log,
.search-results,
.chip-settings-body,
.inv-desc,
.spell-body {
  overscroll-behavior: contain;
}

/* Shared SRD picker styling (inventory, spells, feats, bestiary).
   Lives here so every picker renders identically and a single fix
   (contrast, scrollbar, hover colour) lands everywhere. */
.srd-results {
  display: flex;
  flex-direction: column;
  max-height: 220px;
  overflow-y: auto;
  border: 1px solid var(--grey-dark);
  border-radius: var(--radius-sm);
  background: var(--black-3);
  scrollbar-width: thin;
  scrollbar-color: var(--grey-dark) var(--black-3);
}
.srd-results:empty { display: none; }
.srd-result {
  padding: 8px 12px;
  border-bottom: 1px solid var(--black-4);
  cursor: pointer;
  font-size: 13px;
  color: var(--white);
  line-height: 1.3;
  transition: background var(--transition), color var(--transition);
}
.srd-result:last-child { border-bottom: none; }
/* Selected state — white text on red so the label stays legible.
   Previously rendered as red-bright on red-deep which was invisible
   for anyone with sub-perfect vision. */
.srd-result:hover,
.srd-result:focus {
  background: var(--red);
  color: var(--white);
  outline: none;
}
/* Bundle J — keyboard-only users still need a visible focus ring on
   top of the background swap; the bg alone fails for low-vision
   users at the edge of the contrast envelope. */
.srd-result:focus-visible {
  outline: 2px solid var(--red-bright);
  outline-offset: 1px;
}
.srd-result-meta {
  font-size: 11px;
  color: var(--grey-text);
  font-family: var(--font-display);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  margin-top: 2px;
}
/* Meta line on the hovered row — lift it to a pale tint so it stays
   readable against the red fill without competing with the main label. */
.srd-result:hover .srd-result-meta,
.srd-result:focus .srd-result-meta {
  color: rgba(255, 255, 255, 0.8);
}
.srd-results-note {
  padding: 8px 12px;
  font-size: 12px;
  color: var(--grey-text);
  text-align: center;
  font-style: italic;
}

/* Picker result head — title on the left, source-badge on the right,
   hugging the right edge so the name never gets visually clipped by
   the badge on narrow containers. */
.srd-result-head {
  display: flex;
  align-items: center;
  gap: 8px;
  justify-content: space-between;
}
.srd-result-source {
  flex: 0 0 auto;
  font-size: 10px;
  font-family: var(--font-display);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  padding: 2px 6px;
  border-radius: 10px;
  border: 1px solid var(--black-4);
  background: var(--black-4);
  color: var(--grey-text);
  white-space: nowrap;
}
/* Distinct tints per publisher so the player can scan the list and
   tell SRD from A5E from Kobold at a glance. `background-color` only
   — a tinted border would compete with the hover state. */
.srd-result-source[data-source-key^="srd-"]        { background: rgba(138, 30, 30, 0.22); color: var(--red-bright); border-color: rgba(138, 30, 30, 0.45); }
.srd-result-source[data-source-key^="a5e-"]        { background: rgba(80, 120, 180, 0.22); color: var(--source-a5e); border-color: rgba(80, 120, 180, 0.45); }
.srd-result-source[data-source-key="toh"],
.srd-result-source[data-source-key^="tob"],
.srd-result-source[data-source-key="ccdx"],
.srd-result-source[data-source-key="deepm"],
.srd-result-source[data-source-key="deepmx"],
.srd-result-source[data-source-key="vom"],
.srd-result-source[data-source-key="wz"],
.srd-result-source[data-source-key="kp"],
.srd-result-source[data-source-key="bfrd"]          { background: rgba(180, 130, 40, 0.22); color: #d49a48; border-color: rgba(180, 130, 40, 0.45); }
.srd-result-source[data-source-key="tdcs"]         { background: rgba(90, 140, 90, 0.22); color: #7fb07f; border-color: rgba(90, 140, 90, 0.45); }
.srd-result-source[data-source-key^="open5e"],
.srd-result-source[data-source-key="core"],
.srd-result-source[data-source-key="spells-that-dont-suck"],
.srd-result-source[data-source-key="elderberry-inn-icons"] { background: rgba(140, 100, 180, 0.22); color: #a890cc; border-color: rgba(140, 100, 180, 0.45); }
/* When the row is hovered/selected (red fill), lift badge tints so
   they stay readable without clashing with the red. */
.srd-result:hover .srd-result-source,
.srd-result:focus .srd-result-source {
  background: rgba(255, 255, 255, 0.18);
  color: var(--white);
  border-color: rgba(255, 255, 255, 0.3);
}
.srd-results::-webkit-scrollbar { width: 8px; }
.srd-results::-webkit-scrollbar-track {
  background: var(--black-3);
  border-radius: 4px;
}
.srd-results::-webkit-scrollbar-thumb {
  background: var(--grey-dark);
  border-radius: 4px;
  border: 2px solid var(--black-3);  /* inset effect */
}
.srd-results::-webkit-scrollbar-thumb:hover { background: var(--red); }
.flex { display: flex; }
.row { display: flex; align-items: center; gap: 10px; }
.spread { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
.gap-4 { gap: 4px; } .gap-6 { gap: 6px; } .gap-8 { gap: 8px; } .gap-12 { gap: 12px; }
/* mt-* / mb-* utility scale. The full 2/4/6/8/10/12/16 set is here
   because templates have historically reached for `.mt-4` and
   `.mt-10` thinking they were defined — adding them stops a class
   of silent no-op layout bugs (boxes stacking flush because the
   author wrote mt-10 expecting 10px and got 0px). */
.mt-2 { margin-top: 2px; } .mt-4 { margin-top: 4px; }
.mt-6 { margin-top: 6px; } .mt-8 { margin-top: 8px; }
.mt-10 { margin-top: 10px; } .mt-12 { margin-top: 12px; }
.mt-16 { margin-top: 16px; }
.mb-8 { margin-bottom: 8px; } .mb-12 { margin-bottom: 12px; }

.label {
  font-family: var(--font-display);
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--grey-light);
}

/* ---- Page chrome ---- */
.page-wrap {
  max-width: 480px;
  margin: 0 auto;
  padding-bottom: 120px;
  min-height: 100vh;
  position: relative;
}
/* FAB clearance. The floating dice FAB (and, on the sheet, the
   combat/my-turn FAB) anchors at bottom:~82px+safe, ~54px tall — its TOP
   sits ~136px+safe above the viewport bottom. The base 120px padding
   above is LESS than that, so the last content row (e.g. the Overview
   "Long Rest" button, or the Settings "Show Roll Log" toggle) rendered
   UNDER the FABs. Reserve enough room for the last row to clear them.
   Scoped to `:has(.dd-dice-fab)` — EVERY authenticated surface renders
   the FAB (home tabs, sheet, AND the no-nav focused flows like Settings /
   Account / Profile), so all of them get the clearance. The login page
   does NOT render the FAB (verified: 0 occurrences), so it's excluded and
   keeps the tighter 120px — no scroll reintroduced on the short login
   screen. (`body:has(.bottom-nav)` was too narrow — it missed the
   focused-flow pages, which have the FAB but no bottom-nav.) */
body:has(.dd-dice-fab) .page-wrap {
  padding-bottom: calc(160px + env(safe-area-inset-bottom, 0px));
}

/* Sticky chrome wraps the header AND the tab bar so they stack naturally
   regardless of iOS Safari's collapsing address bar. No magic offsets.
   Horizontal safe-area padding prevents the logo/Home button from being
   clipped by the notch / dynamic island in landscape orientation. */
.sticky-chrome {
  position: sticky;
  top: 0;
  z-index: 100;
  background: var(--black-2);
  padding-top: env(safe-area-inset-top);
  padding-left: env(safe-area-inset-left);
  padding-right: env(safe-area-inset-right);
}

.app-header {
  background: var(--black-2);
  border-bottom: 1px solid var(--red-border);
  /* Asymmetric horizontal padding pulls the logo cluster ~8px closer to
     the left edge; right side keeps enough breathing room for the icon
     cluster not to kiss the viewport edge. */
  padding: 12px 16px 12px 12px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
}

.app-logo {
  display: flex;
  align-items: center;
  /* Gap applies whether the logo is wrapped in an anchor (in-app
     header) or rendered bare (auth pages). Keep it on the outer
     .app-logo so the dragon-to-wordmark spacing is identical
     everywhere. */
  gap: 8px;
  font-family: var(--font-display);
  /* Shrunk ~10% (22 → 20) per product ask — less visual weight, more
     room for the characters list + icons on narrow phones. */
  font-size: 20px;
  font-weight: 700;
  letter-spacing: 0.08em;
  color: var(--white);
  /* Previously a block with an inline-flex child, which let the implicit
     text line-box (22px × 1.5 line-height = 33px) add slack below the
     anchor — pushing the anchor up 5px relative to the icons on the
     right. Flexing the wrapper collapses the line-box and lines the
     left and right clusters up perfectly. */
}

.app-logo-text { white-space: nowrap; }
.app-logo .app-logo-text > span { color: var(--red-bright); }

.app-logo a {
  color: inherit;
  display: inline-flex;
  align-items: center;
  gap: 8px;
}

.app-logo-dragon {
  flex-shrink: 0;
  display: block;
  image-rendering: pixelated;
}

.app-header .header-right {
  display: flex;
  /* Tightened ~20% (8 → 6) so the icon trio reads as one cluster. */
  gap: 6px;
  align-items: center;
}

.section {
  padding: 18px 20px 0;
}

.section-title {
  font-family: var(--font-display);
  font-size: 13px;
  font-weight: 600;
  letter-spacing: 0.15em;
  text-transform: uppercase;
  color: var(--red-bright);
  margin-bottom: 14px;
  padding-bottom: 8px;
  border-bottom: 1px solid var(--grey-dark);
  display: flex;
  align-items: center;
  justify-content: space-between;
  /* Fixed min-height so a title WITH a trailing button (Edit,
     + New, etc.) doesn't sit taller than a bare text title —
     matters on desktop where sections stack side-by-side and
     the underlines need to land on the same y-coordinate.
     44px = btn-sm content (36) + title padding-bottom (8), since
     box-sizing is border-box and min-height includes padding. */
  min-height: 44px;
}

/* ---- Cards ---- */
.card {
  background: var(--black-2);
  border: 1px solid var(--grey-dark);
  border-radius: var(--radius);
  overflow: hidden;
  margin-bottom: 12px;
}

.card-accent {
  border-top: var(--card-accent-top); /* curved-edge accent — OFF; re-enable via --card-accent-top in colors.css */
}

.card-pad { padding: 14px; }

/* ---- Tappable baseline ---- */
.tappable {
  cursor: pointer;
  transition: background var(--transition), border-color var(--transition);
}
.tappable:active {
  background: var(--red-glow);
  border-color: var(--red);
}

/* ──────────────────────────────────────────────────────────────
   Profile / pick-username form layout — input fills the row, Save
   sits inline to its right, status pill drops below. Used by
   templates/social/pick_username.html for the username editor;
   the `.pu-*` prefix scopes these rules to that surface. */
.pu-form { display: flex; flex-direction: column; gap: 6px; }
.pu-field-label {
  font-family: var(--font-display);
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--grey-text);
}
.pu-input-row {
  display: flex;
  gap: 8px;
  align-items: stretch;
}
.pu-input-row .input {
  flex: 1;
  max-width: 360px;
}
.pu-input-row .btn { flex-shrink: 0; }
/* Status pill — appears under the row. JS adds is-ok / is-bad /
   is-checking class to paint the text color. Reserves min-height so
   the row doesn't reflow when a message appears or clears. */
.pu-status {
  font-family: var(--font-display);
  font-size: 12px;
  letter-spacing: 0.04em;
  min-height: 18px;
  color: var(--grey-text);
}
.pu-status.is-ok       { color: var(--green, #2ecc71); }
.pu-status.is-bad      { color: var(--red-bright); }
.pu-status.is-checking { color: var(--grey-text); }

/* Custom-photo upload row on the Profile page — 64px avatar preview
   on the left, Upload / Remove buttons on the right. Same layout
   grammar as .pu-input-row so the section reads as a sibling. */
.pp-photo-row {
  display: flex;
  align-items: center;
  gap: 16px;
}
.pp-photo-preview {
  flex-shrink: 0;
}
.pp-photo-actions {
  display: inline-flex;
  gap: 8px;
  flex-wrap: wrap;
}
.pp-photo-status {
  font-family: var(--font-display);
  font-size: 12px;
  letter-spacing: 0.04em;
  min-height: 18px;
  margin-top: 8px;
  color: var(--grey-text);
}
.pp-photo-status.is-ok       { color: var(--green, #2ecc71); }
.pp-photo-status.is-bad      { color: var(--red-bright); }
.pp-photo-status.is-checking { color: var(--grey-text); }

/* ── About You + Privacy sections (profile expansion 2026-05-18) ──
   Bio + timezone + LFG status + privacy radios. Layout grammar
   matches the .pu-* family above so all four sections (Change
   Username, Custom Photo, About You, Privacy) read as siblings.
   Pronouns was a fourth About-You field in the original C1 (commit
   80d13d6) and was removed on 2026-05-18; the layout still works as
   a 1-up row because .pp-about-field has flex: 1 1 240px so the
   timezone select expands to fill the row alone. */
.pp-about-form { display: flex; flex-direction: column; gap: 6px; }
.pp-about-row {
  display: flex;
  gap: 12px;
  align-items: stretch;
  margin-top: 12px;
  flex-wrap: wrap;
}
.pp-about-field {
  display: flex;
  flex-direction: column;
  gap: 4px;
  flex: 1 1 240px;
  min-width: 0;  /* shrink to zero so flex children don't push overflow */
}
.pp-about-actions {
  display: flex;
  justify-content: flex-end;
  margin-top: 12px;
}
/* LFG + privacy radio strip — borrows the .profile-icon-tile-style
   selected-ring affordance but renders as a horizontal chip row. */
.pp-lfg-row,
.pp-privacy-row {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin-top: 6px;
}
.pp-lfg-chip {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 6px 12px;
  border: 1px solid var(--grey-dark);
  border-radius: var(--radius-sm);
  cursor: pointer;
  font-family: var(--font-display);
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.06em;
  color: var(--grey-text);
  transition: background var(--transition), border-color var(--transition),
              color var(--transition);
  user-select: none;
}
.pp-lfg-chip input[type="radio"] {
  position: absolute;
  opacity: 0;
  pointer-events: none;
}
.pp-lfg-chip:hover {
  border-color: var(--grey-mid);
  color: var(--white);
}
.pp-lfg-chip.selected {
  background: var(--red);
  border-color: var(--red);
  /* Literal #fff per the colors.css inversion caveat — the chip is a
     selected-state pill on a theme-accent fill that doesn't invert
     across modes. Same pattern as .dd-avatar / .header-icon-btn--chip. */
  color: #ffffff;
}
[data-mode="light"] .pp-lfg-chip.selected { color: #ffffff; }

/* ── Account page (user_account.html) — credential change UI ────
   Mirrors the .pu-* form grammar so the Account page reads as a
   sibling of the Profile page. .ua-current renders the read-only
   "current email" row above the change-email form. */
.ua-current {
  display: flex;
  flex-direction: column;
  gap: 4px;
}
.ua-current-val {
  font-family: var(--font-mono);
  font-size: 14px;
  color: var(--white);
  word-break: break-all;  /* long emails wrap rather than overflow */
}
/* Destructive-card chrome — the Delete account section's card carries
   a faint red border-left so the user's eye registers "this is the
   dangerous zone" before they read a single word. Matches the
   .btn-ghost.btn-danger affordance used elsewhere for irreversible
   actions. */
.ua-danger-card {
  border-left: 3px solid var(--red-bright);
}
/* File-input is always visually hidden (Upload button triggers it
   via .click()); LM-1: keep this as a class, never inline
   display:none on the element, so toggle/clear semantics stay
   class-driven. */
.pp-photo-input { display: none; }

/* ---- Inputs ---- */
.input, select, textarea {
  width: 100%;
  background: var(--black-3);
  border: 1px solid var(--grey-dark);
  border-radius: var(--radius);
  padding: 12px 14px;
  color: var(--white);
  font-size: 15px;
  font-family: var(--font-body);
  outline: none;
  transition: border-color var(--transition), box-shadow var(--transition);
}
.input::placeholder, textarea::placeholder { color: var(--grey-mid); }

/* Textareas resize vertically only — width stays locked to the
   parent column. Horizontal resize let users drag a textarea wider
   than its container and broke the surrounding grid layout
   (user-flagged on the Edit Feature modal). Applies globally;
   specific textareas that want a fixed height override with
   resize: none. */
textarea {
  resize: vertical;
}

input:focus, select:focus, textarea:focus {
  border-color: var(--red);
  box-shadow: 0 0 0 3px var(--red-glow);
}

select { appearance: none; background-image: linear-gradient(45deg, transparent 50%, var(--grey-light) 50%),
                                            linear-gradient(135deg, var(--grey-light) 50%, transparent 50%);
         background-position: calc(100% - 18px) 50%, calc(100% - 13px) 50%;
         background-size: 5px 5px, 5px 5px; background-repeat: no-repeat; padding-right: 32px; }

/* Fallback <option> styling — only visible on selects that opt out of
   the JS enhancer via data-dd-select-skip="1". The enhanced dropdown
   (static/js/select_enhance.js + .dd-select-* rules below) is the
   primary path; these rules keep bare-native dropdowns at least
   dark-themed. */
option,
optgroup {
  background-color: var(--black-2);
  color: var(--white);
}
option:checked,
option:hover {
  background-color: var(--red-deep);
  color: var(--white);
}
optgroup {
  font-family: var(--font-display);
  font-weight: 700;
  letter-spacing: 0.08em;
  color: var(--grey-text);
}

/* =============================================================
   Custom <select> — see static/js/select_enhance.js.
   The enhancer hides the real <select> and renders a themed
   trigger + popup. These rules style both, plus rounded corners,
   consistent typography, and theme-reactive highlight colours the
   native popup won't let us touch.
   ============================================================= */
.dd-select-wrap {
  position: relative;
  display: block;
  width: 100%;
  box-sizing: border-box;
}
.dd-select-wrap select.dd-select-enhanced {
  position: absolute;
  inset: 0;
  opacity: 0;
  pointer-events: none;
  /* Keep the select a form participant; visually hidden only. */
  width: 100%;
  height: 100%;
  margin: 0;
  padding: 0;
}
.dd-select-trigger {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  width: 100%;
  background: var(--black-3);
  color: var(--white);
  border: 1px solid var(--grey-dark);
  border-radius: var(--radius);
  padding: 12px 14px;
  font-family: var(--font-body);
  font-size: 15px;
  line-height: 1.2;
  text-align: left;
  cursor: pointer;
  outline: none;
  transition: border-color var(--transition), box-shadow var(--transition);
}
.dd-select-trigger:hover:not([disabled]) { border-color: var(--red-border); }
.dd-select-trigger:focus-visible,
.dd-select-wrap.is-open .dd-select-trigger {
  border-color: var(--red);
  box-shadow: 0 0 0 3px var(--red-glow);
}
.dd-select-trigger[disabled] { opacity: 0.55; cursor: not-allowed; }
.dd-select-label {
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}
.dd-select-label.dd-select-placeholder { color: var(--grey-mid); }
.dd-select-arrow {
  flex: 0 0 auto;
  width: 10px;
  height: 10px;
  background-image:
    linear-gradient(45deg, transparent 50%, var(--grey-light) 50%),
    linear-gradient(135deg, var(--grey-light) 50%, transparent 50%);
  background-position: 0 50%, 5px 50%;
  background-size: 5px 5px, 5px 5px;
  background-repeat: no-repeat;
  transition: transform var(--transition);
}
.dd-select-wrap.is-open .dd-select-arrow {
  transform: rotate(180deg) translateY(-1px);
}
.dd-select-popup {
  /* Default (in-flow) path: mounted INSIDE the wrap as a sibling of
     the trigger. Absolute positioning against the relative wrap anchors
     it under the trigger automatically — DOM flow handles it. */
  position: absolute;
  top: calc(100% + 4px);
  left: 0;
  right: 0;
  display: none;
  z-index: 340;
  max-height: 280px;
  overflow-y: auto;
  background: var(--black-2);
  border: 1px solid var(--grey-dark);
  border-radius: var(--radius);
  /* No internal padding — .dd-select-opt rows carry their own 8px 14px,
     and a vertical gutter here just makes the popup feel taller than the
     options need (feedback ticket 2026-05-07 "Block Padding on Select
     Elements"). The hover/active background of the first/last option now
     extends to the border, which reads cleaner than a stripe of bg-2 above
     it. */
  box-shadow: 0 8px 24px var(--shadow-medium);
  font-family: var(--font-body);
  font-size: 15px;
}
/* If there's no room below (in-flow path), JS toggles `.dd-select-above`
   so the popup anchors from the trigger's top instead of the bottom. */
.dd-select-popup.dd-select-above {
  top: auto;
  bottom: calc(100% + 4px);
}
/* Portaled path: popup is reparented to <body> when an ancestor of the
   wrap would clip it (modals with overflow:hidden, scrollable containers).
   JS writes top/left each frame; fixed positioning escapes the clip. */
.dd-select-popup.dd-select-portaled {
  position: fixed;
  top: 0;
  left: 0;
  right: auto;
  bottom: auto;
  z-index: 9900;
}
.dd-select-popup.is-open { display: block; }
.dd-select-opt {
  padding: 8px 14px;
  color: var(--white);
  cursor: pointer;
  user-select: none;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  transition: background var(--transition), color var(--transition);
}
.dd-select-opt.is-active,
.dd-select-opt:hover:not(.is-disabled) {
  /* Use the theme's primary accent (not --red-deep) so the highlight
     reads on-theme across every palette — amber's --red-deep is a
     chocolate brown which looked off-brand as a selection row. */
  background: var(--red);
  color: var(--white);
}
.dd-select-opt.is-selected {
  color: var(--red-bright);
}
.dd-select-opt.is-selected.is-active,
.dd-select-opt.is-selected:hover {
  color: var(--white);
}
.dd-select-opt.is-disabled {
  color: var(--grey-mid);
  cursor: not-allowed;
}
.dd-select-opt.is-hidden { display: none; }
/* Option rows that carry an info button (set via data-info-body on
   the underlying <option>) — flex layout so the label and the icon
   stay on one line with the icon flush right. The .dd-info-btn
   pulls its own quiet color-only hover from the generic rule. */
.dd-select-opt-with-info {
  display: flex;
  align-items: center;
  gap: 8px;
}
.dd-select-opt-with-info .dd-select-opt-text {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
}
.dd-select-opt-with-info .dd-info-btn {
  width: 22px;
  height: 22px;
  /* Inherit the row's text colour so hover-on-row brightens BOTH
     the text and the icon together, instead of the icon staying
     dim against a now-bright row background. */
  color: inherit;
}

/* Wizard skill-pick rows. Display lives on the class so the
   class-change handler can toggle inline style.display='none'/'' to
   hide/show without trampling the layout (setting display='' falls
   back to this class rule, not the browser <div> default of block).
   */
.dd-skill-row {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 6px 10px;
  background: var(--black-4);
  border: 1px solid var(--grey-deep);
  border-radius: 6px;
}
/* D-3 dedup: skill rows greyed out because the picked background
   already grants this skill. Disabled checkbox + dimmed text + a
   tiny "from <Background>" reason chip rendered inline next to the
   skill name. The grey-with-reason pattern teaches the rule
   instead of silently hiding the option (which is what DDB does on
   their multiclass prereq side). */
.dd-skill-row.dd-skill-row--granted {
  opacity: 0.55;
  background: var(--black-3);
  border-style: dashed;
  border-color: var(--grey-dark);
}
.dd-skill-row.dd-skill-row--granted label {
  cursor: not-allowed;
}
.dd-skill-row .dd-skill-reason {
  display: inline-block;
  margin-left: 6px;
  padding: 1px 6px;
  background: var(--black-2);
  color: var(--red-bright);
  border-radius: var(--radius-sm);
  font-family: var(--font-display);
  font-size: 9px;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}
/* Generic info-button — reused across the character creator (and
   anywhere else a "what is this?" affordance lives next to a
   choice). Kept rectangular per LM-17 (no `border-radius: 50%`).
   Pair every .dd-info-btn with data-info-title / data-info-subtitle
   / data-info-body attributes; the shared modal handler in
   _wizard_base.html reads those and pops the overlay. */
.dd-info-btn {
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 28px;
  height: 28px;
  padding: 0;
  border-radius: var(--radius-sm);
  background: transparent;
  /* Quiet chrome — no visible box. Hover/active flips the icon
     stroke colour only, matching .header-icon-btn's convention
     (the comment up there spells it out: "never render a red/grey
     outline box around them"). The icon SVG uses currentColor so
     theme retinting flows through automatically. */
  border: 1px solid transparent;
  color: var(--grey-text);
  cursor: pointer;
  transition: color var(--transition);
}
.dd-info-btn:hover { color: var(--red-bright); }
/* Bundle J — keyboard-only focus needs a visible ring; color change
   alone fails WCAG 2.1 1.4.11 (non-text contrast) for low-vision
   users navigating without a pointer. Mouse hover keeps the quiet
   chrome (color change only). */
.dd-info-btn:focus-visible {
  color: var(--red-bright);
  outline: 2px solid var(--red-bright);
  outline-offset: 2px;
}
.dd-info-btn:active { color: var(--white); }
.dd-info-btn svg { display: block; }

/* Inline description panel that sits below a wizard <select> and
   updates live when the dropdown value changes. Quiet styling so
   the prose doesn't compete visually with the picker — small
   font, dim colour, generous line-height, pre-wrapped so the
   newlines composed server-side render as paragraph breaks. */
.dd-info-panel {
  margin-top: 8px;
  padding: 10px 12px;
  background: var(--black-3);
  border: 1px solid var(--grey-deep);
  border-radius: var(--radius-sm);
  color: var(--grey-text);
  font-size: 12px;
  line-height: 1.55;
  white-space: pre-wrap;
  display: none;
}

/* Wizard Abilities step. Six cards in a 3-col grid (1-col on
   narrow viewports). Each card centers its label, score dropdown,
   and modifier on a single column so nothing drifts off-axis the
   way the older type=number input did with its native spinner. */
.dd-ability-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(170px, 1fr));
  gap: 8px;
}
.dd-ability-card {
  background: var(--black-2);
  border: 1px solid var(--grey-dark);
  border-radius: var(--radius);
  padding: 12px;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 6px;
}
.dd-ability-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  width: 100%;
}
.dd-ability-name {
  font-family: var(--font-display);
  font-size: 13px;
  font-weight: 700;
  letter-spacing: 0.06em;
  color: var(--white-dim);
}
.dd-ability-score {
  width: 100%;
  text-align: center;
  font-family: var(--font-mono);
  font-size: 22px;
  padding: 6px 8px;
}
.dd-ability-mod {
  font-family: var(--font-mono);
  font-size: 16px;
  color: var(--red-bright);
}

/* Wizard equipment-step category tabs. Re-uses .btn.btn-ghost.btn-sm
   styling for the inactive state so they match the rest of the
   wizard's button vocabulary, then flips background+border to the
   accent red when .is-active is set on the current tab. */
.dd-equip-tab.is-active {
  background: var(--red);
  color: var(--white);
  border-color: var(--red);
}
.dd-select-group {
  padding: 8px 14px 4px;
  font-family: var(--font-display);
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--grey-text);
  pointer-events: none;
}

/* Matches both <label class="field"> and <div class="field"> — the
   enhancer rewrites label-wrapped selects to div to kill click
   forwarding, and that shouldn't change the spacing. */
.field {
  display: block;
  margin-bottom: 12px;
}
.field > .label { display: block; margin-bottom: 6px; }

/* ── +/- stepper (.dd-stepper) — the app-wide pattern ──
   Used any time a numeric value sits between a decrement and an
   increment button (inventory qty, future HP / coin / resource
   steppers). The value cell has a FIXED WIDTH — not min-width — so
   the row layout does NOT shift as the number grows from "1" to
   "198" to "9999". Buttons are also fixed-width so they stay
   tappable even inside cramped rows. Tabular-nums on the value cell
   so individual glyph widths don't jitter between "1" and "8".

   Markup (canonical):
       <div class="dd-stepper" onclick="event.stopPropagation();">
         <button class="dd-stepper-btn" data-delta="-1"
                 aria-label="Decrease">−</button>
         <span class="dd-stepper-val mono">1</span>
         <button class="dd-stepper-btn" data-delta="1"
                 aria-label="Increase">+</button>
       </div>

   DON'T reach for min-width on the value span. DON'T let the span
   size to content. scripts/layout_audit.js flags any .dd-stepper-val
   whose computed width is `auto` (or any min-width-without-width
   pattern that would grow on value change). Any NEW feature in the
   app that needs a +/- counter MUST use this class so the stable
   behaviour is the default. */
.dd-stepper {
  display: inline-flex;
  align-items: center;
  border: 1px solid var(--grey-dark);
  border-radius: var(--radius-sm);
  background: var(--black-3);
  overflow: hidden;
  flex: 0 0 auto;
}
.dd-stepper-btn {
  width: 30px; height: 30px;
  flex: 0 0 30px;
  border: none;
  background: transparent;
  color: var(--grey-text);
  font-family: var(--font-display);
  font-weight: 700;
  font-size: 15px;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  transition: background var(--transition), color var(--transition);
}
.dd-stepper-btn:hover:not([disabled]) { background: var(--black-2); color: var(--white); }
/* Pressed-stepper text MUST be --white per CLAUDE.md filled-chip rule. */
.dd-stepper-btn:active { background: var(--red-deep); color: var(--white); }
.dd-stepper-btn[disabled] { opacity: 0.4; cursor: not-allowed; }
.dd-stepper-val {
  width: 48px;           /* fixed — comfortably fits 5 digits in mono */
  flex: 0 0 48px;
  text-align: center;
  font-size: 13px;
  color: var(--white);
  padding: 0 4px;
  font-variant-numeric: tabular-nums;
  user-select: none;
}

/* ---- Buttons ---- */
.btn {
  font-family: var(--font-display);
  font-size: 13px;
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  padding: 10px 18px;
  border-radius: var(--radius-sm);
  border: none;
  cursor: pointer;
  transition: all var(--transition);
  display: inline-flex;
  align-items: center;
  gap: 6px;
  min-height: 44px;
  text-decoration: none;
}
/* #fff literal, NOT var(--white): colors.css inverts the chrome tokens in
   light mode, so var(--white) here flips the label + currentColor icons to
   dark on the accent fill. White-always on the accent button (cf. ecd0a59f). */
.btn-primary { background: var(--red); color: #ffffff; }
.btn-primary:active, .btn-primary:hover { background: var(--red-bright); box-shadow: 0 0 16px var(--red-glow); }
.btn-ghost { background: transparent; color: var(--grey-text); border: 1px solid var(--grey-dark); }
.btn-ghost:active, .btn-ghost:hover { border-color: var(--red); color: var(--red-bright); }
.btn-sm { padding: 7px 12px; font-size: 11px; min-height: 36px; }
.btn-block { width: 100%; justify-content: center; }

/* Inventory equip / attune toggle — width pin so the row's button
   cluster doesn't jog when state toggles. Button text swaps between
   "Equip" / "Equipped" (4 vs 7 chars) and "Attune" / "Attuned"
   (6 vs 7 chars); without a width floor the row's right-edge cluster
   shifts horizontally and breaks column alignment when scanning the
   inventory list. Same class as the layout_audit `stepper-unstable`
   rule (.dd-stepper-val) — state-toggling controls with content-
   derived intrinsic width need a floor that fits the longest state. */
[data-inv-toggle] {
  min-width: 96px;
  justify-content: center;
}

.flash-list { margin: 12px 20px 0; }
.flash {
  padding: 10px 12px;
  border: 1px solid var(--red-border);
  border-radius: var(--radius-sm);
  background: var(--black-2);
  margin-bottom: 8px;
  font-size: 13px;
  color: var(--white-dim);
}
.flash.success { border-color: var(--red-border); color: var(--white); }
.flash.error { border-color: var(--red); color: var(--red-bright); }

/* ---- Character header card ---- */
.char-header {
  background: var(--black-2);
  border: 1px solid var(--grey-dark);
  border-top: var(--card-accent-top); /* curved-edge accent — OFF; re-enable via --card-accent-top in colors.css */
  border-radius: var(--radius);
  padding: 16px;
  margin-bottom: 12px;
}
.char-name {
  font-family: var(--font-display);
  font-size: 28px;
  font-weight: 700;
  letter-spacing: 0.04em;
  color: var(--white);
  line-height: 1;
}
.char-meta {
  font-size: 12px;
  color: var(--grey-text);
  margin-top: 4px;
  font-family: var(--font-display);
  letter-spacing: 0.06em;
  text-transform: uppercase;
}
.char-meta span { color: var(--grey-light); }

.char-vitals {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 8px;
  margin-top: 16px;
}
.vital {
  background: var(--black-3);
  border: 1px solid var(--grey-dark);
  border-radius: var(--radius-sm);
  padding: 8px 6px;
  text-align: center;
  cursor: pointer;
  transition: border-color var(--transition), background var(--transition);
}
.vital:active { border-color: var(--red); background: var(--red-glow); }
.vital-val {
  font-family: var(--font-display);
  font-size: 22px;
  font-weight: 700;
  color: var(--white);
  line-height: 1;
}
.vital-lbl {
  font-size: 9px;
  color: var(--grey-light);
  text-transform: uppercase;
  letter-spacing: 0.1em;
  margin-top: 3px;
  font-family: var(--font-display);
}

/* HP block — two rows so the bar gets max horizontal real estate. */
.hp-block {
  background: var(--black-3);
  border: 1px solid var(--grey-dark);
  border-radius: var(--radius);
  padding: 12px 14px;
  margin-top: 12px;
  display: flex;
  flex-direction: column;
  gap: 10px;
}
.hp-main {
  display: flex;
  align-items: center;
  gap: 12px;
}
.hp-label {
  font-family: var(--font-display);
  font-size: 11px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--grey-light);
  min-width: 24px;
  flex-shrink: 0;
}
.hp-bar-wrap {
  flex: 1 1 auto;
  height: 12px;                   /* bumped from 8 — more presence */
  background: var(--black-4);
  border-radius: 6px;
  overflow: hidden;
  min-width: 0;
  box-shadow: inset 0 1px 2px var(--shadow-soft);
}
.hp-bar {
  height: 100%;
  border-radius: 6px;
  /* Background colour is set by JS (hp_controls.js) and interpolated
     from grey-mid at full HP to red-bright at 0 HP. Server-rendered
     initial value is applied via the inline `background-color` style
     attribute on the element, so first paint shows the right shade. */
  background-color: #666;
  transition:
    width 0.35s cubic-bezier(0.4, 0, 0.2, 1),
    background-color 0.35s ease;
}
.hp-nums {
  font-family: var(--font-mono);
  font-size: 15px;
  color: var(--white);
  min-width: 68px;
  text-align: right;
  flex-shrink: 0;
  font-variant-numeric: tabular-nums;  /* stops the label jittering as digits change */
}

/* Damage / heal button row */
.hp-controls {
  display: flex;
  gap: 6px;
  justify-content: flex-end;
}
.hp-btn {
  flex: 1 1 0;
  min-width: 54px;
  max-width: 80px;
  height: 38px;
  border-radius: var(--radius-sm);
  font-family: var(--font-display);
  font-size: 14px;
  font-weight: 700;
  letter-spacing: 0.04em;
  cursor: pointer;
  transition:
    background 80ms ease,
    border-color 80ms ease,
    transform 60ms ease;
  -webkit-tap-highlight-color: transparent;
  touch-action: manipulation;
  user-select: none;
  -webkit-user-select: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}
/* Damage / heal buttons intentionally use HARD-CODED red and green —
   they're semantic (red = losing HP, green = gaining HP) and must not
   flip to emerald / sapphire / rose when the user picks a theme. The
   accent-colour swap applies to the app chrome, not combat semantics. */
.hp-btn-dmg {
  background: rgba(204,34,34,0.12);
  border: 1px solid rgba(204,34,34,0.4);
  color: #e63333;
}
.hp-btn-dmg:hover { background: rgba(204,34,34,0.22); }
.hp-btn-dmg:active,
.hp-btn-dmg.pressed {
  background: #cc2222;
  border-color: #cc2222;
  color: #ffffff;
  transform: scale(0.96);
}
.hp-btn-heal {
  background: rgba(60,180,90,0.12);
  border: 1px solid rgba(60,180,90,0.4);
  color: var(--success-text);
}
.hp-btn-heal:hover { background: rgba(60,180,90,0.22); }
.hp-btn-heal:active,
.hp-btn-heal.pressed {
  background: var(--success);
  border-color: var(--success);
  color: var(--white);
  transform: scale(0.96);
}

/* Subtle flash on the HP bar when it changes — reinforces that a tap
   registered, even if the value didn't actually clamp. */
@keyframes hpFlash {
  0%   { box-shadow: 0 0 0 0 var(--red-glow); }
  100% { box-shadow: 0 0 0 6px transparent; }
}
.hp-bar.flash { animation: hpFlash 0.45s ease-out; }

/* ---- Universal search ---- */
.search-wrap { position: relative; margin-bottom: 4px; }
.search-input {
  width: 100%;
  background: var(--black-3);
  border: 1px solid var(--grey-dark);
  border-radius: var(--radius);
  padding: 11px 14px 11px 38px;
  color: var(--white);
  font-size: 15px;
  font-family: var(--font-body);
  outline: none;
  transition: border-color var(--transition), box-shadow var(--transition);
}
.search-input:focus { border-color: var(--red); box-shadow: 0 0 0 3px var(--red-glow); }
.search-input::placeholder { color: var(--grey-mid); }
.search-icon {
  position: absolute;
  left: 12px; top: 50%;
  transform: translateY(-50%);
  color: var(--grey-mid);
  font-size: 14px;
  pointer-events: none;
}
/* X clear button — mirrors the search-icon's vertical placement but
   on the right edge. Hidden via `.hide` when the input is empty; the
   JS toggles it on every input event. Sized so tap target comfortably
   exceeds 32px on mobile; hover brightens for desktop feedback. */
.search-clear {
  position: absolute;
  right: 6px; top: 50%;
  transform: translateY(-50%);
  background: transparent;
  border: 0;
  width: 32px; height: 32px;
  display: inline-flex; align-items: center; justify-content: center;
  color: var(--grey-mid);
  border-radius: var(--radius-sm);
  cursor: pointer;
  transition: color var(--transition), background var(--transition);
}
.search-clear:hover,
.search-clear:focus-visible {
  color: var(--white);
  background: var(--black-3);
  outline: none;
}
.search-clear:active { color: var(--red-bright); }
.search-clear.hide { display: none; }
/* Reserve right-edge space in the input for the X button so text
   doesn't overlap the icon when long queries scroll into view. */
.search-input { padding-right: 40px; }

/* Suppress the native HTML5 search-input cancel button. Browsers
   render `<input type="search">` with a small native X on the right
   edge that we can't fully style — it has its own size, position,
   and color rules that ignore our padding and bleed past the
   focus-ring on some viewports (the rules page + every
   campaign-builder browse field uses type="search" and was leaking
   the native X past the orange focus ring on desktop). Surfaces
   that need a clear affordance wire their own .search-clear button
   (see /sheet's universal search). */
input[type="search"]::-webkit-search-cancel-button,
input[type="search"]::-webkit-search-decoration,
input[type="search"]::-webkit-search-results-button,
input[type="search"]::-webkit-search-results-decoration {
  -webkit-appearance: none;
          appearance: none;
}
.search-results {
  background: var(--black-2);
  border: 1px solid var(--red-border);
  border-radius: var(--radius);
  margin-top: 6px;
  overflow: hidden;
  box-shadow: 0 8px 24px var(--shadow-60);
  position: absolute;
  left: 0; right: 0;
  z-index: 200;
  /* Cap by dynamic viewport so iOS Safari doesn't push the dropdown
     under the URL bar when the user starts typing. */
  max-height: calc(var(--dd-vh) * 0.6);
  overflow-y: auto;
}
.search-result {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 11px 14px;
  border-bottom: 1px solid var(--black-4);
  cursor: pointer;
  min-height: 52px;
}
.search-result:last-child { border-bottom: none; }
.search-result:active, .search-result:hover { background: var(--red-glow); }
.sr-left { display: flex; flex-direction: column; min-width: 0; }
.sr-name { font-family: var(--font-display); font-size: 15px; font-weight: 600; color: var(--white); }
.sr-sub { font-size: 11px; color: var(--grey-text); margin-top: 1px; }
.sr-right { display: flex; align-items: center; gap: 10px; }
.sr-val, .sr-mod { font-family: var(--font-mono); font-size: 20px; color: var(--red-bright); font-weight: 700; }

.roll-pill {
  background: var(--red);
  color: var(--white);
  font-family: var(--font-display);
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  padding: 6px 12px;
  border-radius: 20px;
  border: none;
  cursor: pointer;
  min-height: 36px;
}
.roll-pill:active { background: var(--red-bright); }

/* ---- Stats grid + skills ---- */
.stats-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 8px;
}
.stat-card {
  background: var(--black-2);
  border: 1px solid var(--grey-dark);
  border-radius: var(--radius);
  padding: 12px 8px;
  text-align: center;
  cursor: pointer;
  transition: all var(--transition);
  position: relative;
}
.stat-card:active { border-color: var(--red); background: var(--red-glow); }
.stat-abbr {
  font-family: var(--font-display);
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.15em;
  text-transform: uppercase;
  color: var(--grey-light);
}
.stat-score {
  font-family: var(--font-display);
  font-size: 28px;
  font-weight: 700;
  color: var(--white);
  line-height: 1;
  margin: 4px 0;
}
.stat-mod { font-family: var(--font-mono); font-size: 16px; color: var(--red-bright); }

.skills-list, .rows-list {
  background: var(--black-2);
  border: 1px solid var(--grey-dark);
  border-radius: var(--radius);
  overflow: hidden;
}
.skill-row, .data-row {
  display: flex;
  align-items: center;
  padding: 10px 14px;
  border-bottom: 1px solid var(--black-4);
  cursor: pointer;
  transition: background var(--transition);
  gap: 10px;
  min-height: 44px;
}
.skill-row:last-child, .data-row:last-child { border-bottom: none; }
.skill-row:active, .data-row:active { background: var(--red-glow); }
/* `.data-row.is-button` — when the row is rendered as a <button>
   instead of an <a> (e.g. opens a picker modal). Strips the native
   button chrome so it visually matches the anchor variant. Lives here
   so the reset doesn't drift across template copies. */
.data-row.is-button {
  width: 100%;
  background: transparent;
  /* Reset the native <button> chrome on three sides ONLY — KEEP the
     .data-row border-bottom so the row divider still renders inside a
     .rows-list. `border: none` here stripped it, so Export (More) +
     Learn & Feedback (settings) read as one card with NO lines between
     rows (2026-06-18 user report). The last row still drops the divider
     via the base `.data-row:last-child` rule. */
  border-top: 0;
  border-left: 0;
  border-right: 0;
  color: inherit;
  font: inherit;
  text-align: left;
  cursor: pointer;
}
.skill-name { flex: 1; font-size: 14px; color: var(--white-dim); font-family: var(--font-body); }
.skill-attr { font-size: 11px; color: var(--grey-mid); min-width: 28px; font-family: var(--font-display); letter-spacing: 0.05em; text-transform: uppercase; }
.skill-mod { font-family: var(--font-mono); font-size: 15px; font-weight: 700; color: var(--red-bright); min-width: 32px; text-align: right; }

.prof-dot, .skill-dot {
  width: 8px; height: 8px;
  border-radius: 50%;
  flex-shrink: 0;
  border: 1.5px solid var(--grey-mid);
  background: transparent;
}
.prof-dot.proficient, .skill-dot.prof { background: var(--red); border-color: var(--red); }
.prof-dot.expertise, .skill-dot.exp { background: var(--red-bright); border-color: var(--red-bright); box-shadow: 0 0 6px var(--red); }

/* ---- Spell slots ----
   Each level gets a card showing the level label, a "remaining/max"
   count, and a row of chunky bubbles (each is a real 28×28 tap target,
   not a cosmetic 12 px dot). Filled bubble = slot available; outlined
   = spent. Tapping any bubble toggles it — optimistic update in JS so
   there's no lag between tap and visual feedback. */
.spell-slots {
  display: flex;
  gap: 8px;
  flex-wrap: nowrap;
  overflow-x: auto;
  margin-bottom: 12px;
  padding-bottom: 6px;
}
.slot-group {
  background: var(--black-2);
  border: 1px solid var(--grey-dark);
  border-top: var(--card-accent-top); /* curved-edge accent — OFF; re-enable via --card-accent-top in colors.css */
  border-radius: var(--radius-sm);
  padding: 10px 12px 12px;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 6px;
  min-width: 84px;
  flex-shrink: 0;
}
.slot-level {
  font-family: var(--font-display);
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--red-bright);
}
.slot-count {
  font-family: var(--font-mono);
  font-size: 13px;
  color: var(--white);
  line-height: 1;
}
.slot-count .slot-count-total { color: var(--grey-text); font-size: 11px; }
.slot-bubbles {
  display: flex;
  gap: 6px;
  flex-wrap: wrap;
  justify-content: center;
  margin-top: 2px;
}

/* LM-17 exception: .bubble is a use-indicator pip (spell slot / feature
   uses-remaining), same vocabulary as a die pip. Pip-style indicators
   are a listed LM-17 exception. */
.bubble {
  width: 28px;
  height: 28px;
  min-height: 28px;        /* beat any inherited mobile rule */
  border-radius: 50%;
  border: 2px solid var(--red);
  background: var(--red);
  cursor: pointer;
  padding: 0;
  box-shadow: inset 0 0 0 2px rgba(255,255,255,0.08), 0 0 6px rgba(204,34,34,0.35);
  transition: transform 0.12s ease, background var(--transition),
              border-color var(--transition), box-shadow var(--transition);
  flex-shrink: 0;
}
.bubble.empty {
  background: transparent;
  border-color: var(--grey-mid);
  box-shadow: none;
}
.bubble:hover { transform: scale(1.08); }
.bubble:active { transform: scale(0.92); }

/* ---- Spell-list group header (Cantrips / 1st Level Spells / etc) ---- */
.spell-group-header {
  font-family: var(--font-display);
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--red-bright);
  margin: 14px 2px 6px;
  padding-bottom: 4px;
  border-bottom: 1px solid var(--grey-dark);
}
.spell-group-header:first-child { margin-top: 0; }

/* ---- Spell cards ---- */
.spell-card {
  background: var(--black-2);
  border: 1px solid var(--grey-dark);
  border-radius: var(--radius);
  overflow: hidden;
  margin-bottom: 8px;
  transition: border-color var(--transition);
}
.spell-card:active { border-color: var(--red); }
.spell-card-head {
  display: flex;
  align-items: center;
  padding: 11px 14px;
  gap: 10px;
  cursor: pointer;
}
.spell-level-badge {
  background: var(--black-4);
  border: 1px solid var(--grey-dark);
  border-radius: var(--radius-sm);
  font-family: var(--font-mono);
  font-size: 11px;
  color: var(--grey-text);
  padding: 2px 6px;
  min-width: 28px;
  text-align: center;
  flex-shrink: 0;
}
.spell-name { flex: 1; font-family: var(--font-display); font-size: 16px; font-weight: 600; color: var(--white); }
.spell-tags { display: flex; gap: 4px; align-items: center; flex-shrink: 0; }
.spell-tag { font-family: var(--font-display); font-size: 9px; font-weight: 700; letter-spacing: 0.1em; padding: 2px 5px; border-radius: 2px; text-transform: uppercase; }
.spell-tag.conc { background: var(--red-deep); color: var(--white); }
.spell-tag.ritual { background: var(--black-4); color: var(--grey-light); border: 1px solid var(--grey-dark); }
.spell-tag.V, .spell-tag.S, .spell-tag.M { background: var(--black-3); color: var(--grey-text); border: 1px solid var(--grey-dark); min-width: 18px; text-align: center; }
.spell-tag.M.costly { color: var(--red-bright); border-color: var(--red-border); }
/* Source-attribution pill — neutral grey, slightly wider, no
   uppercase scrunch (publisher names are sentence case). Tooltip
   carries the full source-document name. */
.spell-tag.src-pill {
  font-family: var(--font-display);
  font-weight: 600;
  font-size: 9px;
  letter-spacing: 0.06em;
  text-transform: none;
  padding: 2px 6px;
  background: var(--black-2);
  border: 1px solid var(--grey-dark);
  color: var(--grey-text);
}
/* 2024 weapon mastery pill — small green tag next to equipped
   weapons whose mastery the character has selected at level-up.
   Tooltip carries the mechanical effect text. */
.weapon-mastery-pill {
  display: inline-block;
  font-family: var(--font-display);
  font-weight: 700;
  font-size: 9px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  padding: 2px 6px;
  margin-left: 6px;
  border-radius: 3px;
  background: rgba(50, 140, 70, 0.15);
  border: 1px solid rgba(50, 140, 70, 0.45);
  color: #6ec97e;
  vertical-align: middle;
}

.spell-meta-row { display: flex; gap: 12px; padding: 0 14px 10px; font-size: 12px; color: var(--grey-text); font-family: var(--font-display); letter-spacing: 0.04em; flex-wrap: wrap; }
.spell-meta-row span { color: var(--white-dim); }

.spell-body { padding: 12px 14px; border-top: 1px solid var(--black-4); font-size: 13px; color: var(--grey-text); line-height: 1.6; white-space: pre-wrap; }
.spell-body p { margin-bottom: 8px; }
.spell-actions { display: flex; justify-content: flex-end; gap: 8px; padding: 10px 14px; border-top: 1px solid var(--black-4); }

/* Prepared marker on spell card — proper 24 px circle sized the same
   as the other tap targets on the card. Was 10 px but mobile's
   min-height:36px rule forced it into a 10×36 oval; now it sets its
   own min-height explicitly. Filled = prepared for casting. */
.prep-dot {
  width: 24px;
  height: 24px;
  min-height: 24px;
  padding: 0;
  border-radius: 50%;
  border: 2px solid var(--grey-mid);
  background: transparent;
  cursor: pointer;
  flex-shrink: 0;
  transition: background var(--transition), border-color var(--transition), transform 0.12s ease;
}
.prep-dot:hover { border-color: var(--red-border); transform: scale(1.08); }
.prep-dot:active { transform: scale(0.92); }
.prep-dot.on {
  background: var(--red);
  border-color: var(--red);
  box-shadow: 0 0 6px rgba(204,34,34,0.4);
}

/* ---- Concentration banner ---- */
/* LM-21 realtime: hide the always-mounted banner shell when no
   concentration is active. The shell stays in the DOM so the realtime
   chrome listener (in _sheet_base.html) can flip data-conc-active=1
   without rebuilding the structure when the user casts a concentration
   spell. CSS-driven visibility — no inline style mutation, no JS
   re-render. */
#conc_banner_wrap[data-conc-active="0"] { display: none; }
.conc-banner {
  background: linear-gradient(90deg, var(--red-deep), var(--black-3));
  border: 1px solid var(--red-border);
  border-radius: var(--radius);
  padding: 10px 14px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  margin-bottom: 12px;
}
.conc-left { display: flex; align-items: center; gap: 10px; min-width: 0; flex: 1; }
.conc-dot {
  width: 8px; height: 8px;
  border-radius: 50%;
  background: var(--red-bright);
  box-shadow: 0 0 8px var(--red);
  animation: pulse 1.8s ease-in-out infinite;
  flex-shrink: 0;
}
.conc-spell { font-family: var(--font-display); font-weight: 700; letter-spacing: 0.05em; color: var(--white); }
.conc-dur { font-family: var(--font-mono); color: var(--grey-text); font-size: 12px; }

@keyframes pulse {
  0%, 100% { opacity: 1; box-shadow: 0 0 8px var(--red); }
  50% { opacity: 0.6; box-shadow: 0 0 3px var(--red); }
}

/* ---- NPC cards ---- */
.npc-card { background: var(--black-2); border: 1px solid var(--grey-dark); border-left: 3px solid var(--grey-mid); border-radius: var(--radius); padding: 12px 14px; margin-bottom: 8px; }
.npc-card.friendly { border-left-color: #2a7a2a; }
.npc-card.hostile  { border-left-color: var(--red); }
.npc-card.neutral  { border-left-color: var(--grey-mid); }
.npc-card.complex  { border-left-color: #8b6914; }
.npc-card.unknown  { border-left-color: var(--grey-dark); }
.npc-name { font-family: var(--font-display); font-size: 16px; font-weight: 700; color: var(--white); }
.npc-meta { font-size: 11px; color: var(--grey-light); margin-top: 2px; font-family: var(--font-display); text-transform: uppercase; letter-spacing: 0.08em; }

/* 3.2 — World-state chip. Surfaces NPC.state_flag / Location.state_flag
   on the list view + edit form. Filled chip on accent backgrounds:
   text MUST be --white (CLAUDE.md filled-chip rule). LM-17:
   rectangular, --radius-sm. Used as both a non-interactive marker
   chip on cards and a clickable pill-row inside edit forms. */
.dd-state-chip {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 3px 8px;
  font-family: var(--font-display);
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  border-radius: var(--radius-sm);
  border: 1px solid var(--grey-dark);
  background: var(--black-3);
  color: var(--grey-text);
  white-space: nowrap;
  line-height: 1.3;
  min-height: 22px;
}
.dd-state-chip.is-status-dead,
.dd-state-chip.is-status-missing,
.dd-state-chip.is-status-captured,
.dd-state-chip.is-status-abandoned,
.dd-state-chip.is-stance-hostile,
.dd-state-chip.is-stance-rival,
.dd-state-chip.is-intact-false {
  background: var(--red-deep);
  border-color: var(--red);
  color: var(--white); /* LM filled-chip rule */
}
.dd-state-chip.is-status-alive,
.dd-state-chip.is-status-peaceful,
.dd-state-chip.is-stance-friendly,
.dd-state-chip.is-stance-ally {
  background: rgba(42,122,42,0.22);
  border-color: rgba(60,200,90,0.55);
  color: #9ee39c;
}
.dd-state-chip.is-status-contested,
.dd-state-chip.is-stance-neutral {
  background: rgba(139,105,20,0.22);
  border-color: rgba(200,160,60,0.55);
  color: #e4ce7a;
}

/* Toggle row inside the NPC/Location edit form — small tappable
   pill-rectangles, one per state value. Selected = filled red,
   unselected = ghost. LM-17: --radius-sm rectangles. */
.dd-state-toggle {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin: 4px 0 10px;
}
.dd-state-toggle-opt {
  display: inline-flex;
  align-items: center;
  padding: 6px 10px;
  font-family: var(--font-display);
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  border-radius: var(--radius-sm);
  border: 1px solid var(--grey-dark);
  background: var(--black-3);
  color: var(--grey-text);
  cursor: pointer;
  user-select: none;
  min-height: 32px;
  transition: all var(--transition);
}
.dd-state-toggle-opt:hover { color: var(--white); border-color: var(--grey-mid); }
.dd-state-toggle-opt.is-active {
  background: var(--red-deep);
  border-color: var(--red);
  color: var(--white); /* LM filled-chip rule */
}
.npc-notes { font-size: 13px; color: var(--grey-text); margin-top: 6px; line-height: 1.5; }

/* ---- Per-paragraph DM-visibility blocks (Pain #48) ---- */
/* Editor: each block is a card-pad with textarea + DM-only checkbox.
   Render: same prose colour as .npc-notes; DM-only blocks get a
   left red border + DM ONLY chip so the DM scans which paragraphs
   are private. Players never see the chip — server gates render. */
.db-editor { display: flex; flex-direction: column; gap: 10px; margin: 8px 0 12px; }
.db-editor [data-db-blocks] { display: flex; flex-direction: column; gap: 8px; }
.db-block { background: var(--black-3); }
.db-block textarea.input { min-height: 64px; resize: vertical; }
.db-render {
  font-size: 13px;
  color: var(--grey-text);
  margin-top: 8px;
  line-height: 1.5;
  white-space: pre-wrap;
}
.db-render + .db-render { margin-top: 8px; }
.db-render-dm {
  position: relative;
  background: rgba(180, 32, 32, 0.06);
  border-left: 3px solid var(--red);
  padding: 6px 10px 6px 12px;
  border-radius: var(--radius-sm);
}
.db-render-dm .db-text { margin-top: 4px; }
.chip.db-dm-chip {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 2px 8px;
  background: var(--red-deep);
  color: var(--white);
  border: 1px solid var(--red);
  border-radius: var(--radius-sm);
  font-family: var(--font-display);
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  margin-bottom: 2px;
}
.chip.db-dm-chip svg { color: currentColor; }

/* ---- Resource chips ---- */
.resource-bar {
  display: flex;
  gap: 8px;
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
  padding-bottom: 4px;
  margin-bottom: 12px;
  scrollbar-width: none;
}
.resource-bar::-webkit-scrollbar { display: none; }

.resource-chip {
  flex-shrink: 0;
  background: var(--black-2);
  border: 1px solid var(--grey-dark);
  border-radius: var(--radius);
  padding: 8px 12px;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
  cursor: pointer;
  min-width: 70px;
  transition: border-color var(--transition);
}
.resource-chip:active { border-color: var(--red); }
.resource-name {
  font-family: var(--font-display);
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--grey-light);
}
.resource-value { font-family: var(--font-mono); font-size: 16px; color: var(--red-bright); font-weight: 700; }

/* ---- Roll log ---- */
.roll-log {
  background: var(--black-2);
  border: 1px solid var(--grey-dark);
  border-radius: var(--radius);
  overflow: hidden;
  max-height: 300px;
  overflow-y: auto;
}
.roll-entry {
  display: flex;
  align-items: center;
  padding: 9px 14px;
  border-bottom: 1px solid var(--black-4);
  gap: 10px;
  min-height: 44px;
}
.roll-entry:last-child { border-bottom: none; }
.roll-context {
  flex: 1;
  font-size: 12px;
  color: var(--grey-text);
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.roll-notation { font-family: var(--font-mono); font-size: 11px; color: var(--grey-mid); }
.roll-total {
  font-family: var(--font-mono);
  font-size: 18px;
  font-weight: 700;
  color: var(--white);
  min-width: 36px;
  text-align: right;
}
.roll-total.crit { color: var(--red-bright); text-shadow: 0 0 10px var(--red); }
.roll-total.fumble { color: var(--grey-mid); }

/* ---- iOS keyboard fix ----
   iOS Safari re-parents `position: fixed; bottom: 0` elements onto the
   visual viewport when the on-screen keyboard opens, so the bottom
   nav ends up floating above the keyboard mid-screen. The pre-paint
   script in base.html flags `html.dd-kb-open` whenever the visual
   viewport shrinks enough to indicate a keyboard; we hide every
   bottom-anchored fixed element while the flag is on. Users don't
   need the bottom chrome while typing, and it comes back on dismiss.
   Same fix applies to any NEW bottom-anchored fixed element — add it
   to this list. See LM-3 in CLAUDE.md. */
html.dd-kb-open .bottom-nav,
html.dd-kb-open .dd-owl-peek,
html.dd-kb-open .my-turn-fab,
html.dd-kb-open .npc-fab,
html.dd-kb-open .dd-dice-fab,
html.dd-kb-open .install-hint { display: none !important; }

/* ---- Sync footer (Pain #15) ----
   Tiny "Synced X ago" line at the bottom of every sheet. Lives in
   normal document flow, sits above the bottom-nav reservation so it
   doesn't get covered. Right-aligned + low contrast so it confirms
   freshness without competing with sheet content for attention. */
.dd-sync-footer {
  text-align: right;
  padding: 6px 12px 12px;
  margin: 4px 0 0;
  font-family: var(--font-display);
  letter-spacing: 0.02em;
  opacity: 0.7;
}

/* ---- Bottom nav ----
   iPhones have rounded bottom corners and a home-indicator bar. We
   inset the whole nav with env(safe-area-inset-*) so the leftmost and
   rightmost items (Overview / Campaign) don't get clipped by the
   corner curve. Labels are also clamped so they never overflow a
   narrow viewport (e.g. iPhone SE at 320 px). */
.bottom-nav {
  /* LM-3 ok: .bottom-nav is registered in the html.dd-kb-open keyboard
     hide-list (base.css ~L2203), so it's hidden while the iOS keyboard
     is up — the fixed-bottom-chrome requirement is satisfied. */
  position: fixed;
  bottom: 0;
  left: 0;
  right: 0;
  max-width: 480px;
  margin: 0 auto;
  background: var(--black-2);
  border-top: 1px solid var(--grey-dark);
  display: flex;
  z-index: 60;
  padding-left: env(safe-area-inset-left);
  padding-right: env(safe-area-inset-right);
  padding-bottom: env(safe-area-inset-bottom);
  height: calc(64px + env(safe-area-inset-bottom));
  /* NO compositor-layer promotion here (no translateZ(0) / will-change).
     The old hack was meant to keep the nav pinned during iOS momentum-
     scroll, but it BACKFIRES inside the Capacitor WKWebView: a promoted
     position:fixed layer is offset by the native contentInset:'always'
     at the scroll extremes, so the nav "jumped out of place" the moment
     you scrolled to the bottom. The clean pin is just position:fixed +
     `overscroll-behavior-y: none` on html/body (base.css ~L281), which
     already kills the bounce the hack was compensating for. See LM-3. */
}
.nav-item {
  flex: 1 1 0;
  min-width: 0;              /* let flex shrink so long labels don't overflow */
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 3px;
  color: var(--grey-mid);
  border: none;
  background: none;
  padding: 0 2px;            /* tiny side padding so labels don't touch dividers */
  cursor: pointer;
  transition: color var(--transition);
  min-height: 64px;
  text-decoration: none;
  overflow: hidden;
}
.nav-item.active { color: var(--red-bright); }
.nav-item svg { width: 20px; height: 20px; }
.nav-badge-host { position: relative; }
.nav-badge {
  position: absolute;
  top: 8px;
  right: calc(50% - 22px);
  min-width: 16px;
  height: 16px;
  padding: 0 4px;
  /* LM-17: count-badge defaults to rectangle; was 999px pill. */
  border-radius: var(--radius-sm);
  background: var(--red);
  color: var(--white);
  font-family: var(--font-display);
  font-size: 10px;
  font-weight: 700;
  line-height: 16px;
  text-align: center;
}
.nav-lbl {
  font-family: var(--font-display);
  /* Clamp font size between 8.5px (tiny phones) and 10px (large phones). */
  font-size: clamp(8.5px, 2.3vw, 10px);
  font-weight: 700;
  letter-spacing: 0.06em;    /* slightly tighter than 0.1em so 9-char labels fit */
  text-transform: uppercase;
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* ---- FAB ---- */
/* LM-17 exception: FABs (.dd-dice-fab in base.html, .my-turn-fab,
   .npc-fab) intentionally use border-radius: 50% per the floating-
   action-button design vocabulary (round, distinct from in-flow
   rectangular buttons). Brand-defining shape — keep round. */
/* My Turn FAB — 54×54 circle matching .dd-dice-fab, parked
   immediately to its left on the bottom-right so the two FABs read
   as a paired cluster (18px from right edge + 54px dice width +
   10px gap = 82px from right). Icon-only — label moves to
   aria-label + title tooltip. */
.my-turn-fab {
  position: fixed;
  bottom: calc(18px + env(safe-area-inset-bottom, 0px));
  right: 82px;
  width: 54px;
  height: 54px;
  padding: 0;
  background: var(--red);
  color: #ffffff;  /* white-always: var(--white) inverts to dark ink in light mode, blacking out the icon */
  border: 1px solid var(--red-border);
  border-radius: 50%;
  cursor: pointer;
  box-shadow: 0 4px 14px var(--shadow-45);
  z-index: 55;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  transition: transform 0.15s ease, background var(--transition);
}
.my-turn-fab:active { transform: scale(0.96); box-shadow: 0 2px 10px var(--shadow-35); }
/* Lift above the bottom-nav on pages that have one — same offset
   math as .dd-dice-fab so the pair stays aligned. */
body:has(.bottom-nav) .my-turn-fab {
  bottom: calc(82px + env(safe-area-inset-bottom, 0px));
}
/* When Virtual Dice is OFF, the dice slot (right:18px) is empty — slide the
   My Turn FAB into it instead of leaving it floating at right:82px with dead
   space to its right. Re-enabling dice simply stops this rule applying, so
   My Turn returns to its paired left position. (2026-06-17) */
html.dd-virtual-dice-off .my-turn-fab { right: 18px; }

.npc-fab {
  position: fixed;
  bottom: calc(80px + env(safe-area-inset-bottom));
  left: 20px;
  width: 48px; height: 48px;
  background: var(--black-2);
  color: var(--grey-text);
  border: 1px solid var(--red-border);
  border-radius: 50%;
  cursor: pointer;
  z-index: 55;
  display: flex;
  align-items: center;
  justify-content: center;
  box-shadow: 0 4px 16px var(--shadow-50);
}
.npc-fab:active { color: var(--red-bright); border-color: var(--red); }

/* ---- My Turn modal ---- */
/* My Turn modal — full-screen flex column.
   Safe-area-aware so content doesn't slide under the home indicator
   or the iOS toolbar. See safe-area framework at top of file. */
.my-turn-modal {
  position: fixed;
  inset: 0;
  background: var(--overlay-deep);
  z-index: 300;
  display: none;
  flex-direction: column;
  /* Use dvh so the modal sizes correctly when iOS Safari's URL bar
     pops up; without this it's set to 100vh and overflows. */
  height: var(--dd-vh);
  max-height: var(--dd-vh);
}
.my-turn-modal.open { display: flex; }
/* ── Safe-area convention for .modal-header / .modal-body / .modal-footer ──
   These classes are SHARED between two modal patterns and the inner
   padding MUST NOT bake in env(safe-area-inset-*) at the base level,
   or modals opened via .dd-modal-overlay will double-inset (once from
   the overlay padding, once from the inner class) and show empty
   strips on notched phones (user-reported: Add Item modal on iPhone).

   Rules:
     • .dd-modal-overlay modals (centered card, the common case) —
       overlay pads for the notch/home-indicator; header/body/footer
       stay safe-area-neutral.
     • .my-turn-modal (edge-to-edge full-screen) — no overlay; header
       + body + footer add their own safe-area insets via the scoped
       rules below.

   When you build a new modal: use the .dd-modal-overlay + .dd-modal-card
   pattern and let the overlay handle safe-area. Do NOT hand-roll a
   full-screen modal without replicating the .my-turn-modal scope
   below. scripts/layout_audit.js checks for the anti-pattern. */
/* `.modal-head` is the canonical class per CLAUDE.md (modal anatomy
   section). `.modal-header` is the legacy spelling that's still used
   in ~30 templates. Both selectors here so existing markup keeps
   working while the CLAUDE.md-recommended spelling also gets the
   safe-area / two-tone-bar treatment. */
.modal-header,
.modal-head {
  background: var(--black-2);
  border-bottom: 2px solid var(--red);
  padding: 14px 20px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-shrink: 0;
}
.modal-title {
  font-family: var(--font-display);
  font-size: 18px;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--white);
}
.modal-close {
  /* 32×32: deliberately below Apple HIG's 44px touch-target guideline.
     Still passes WCAG 2.2 SC 2.5.8 Target Size Minimum (24×24 floor).
     The button sits in modal chrome with empty space around it (no
     adjacent tap targets), so mistap risk is low; html.dd-large-taps
     re-floors it back to 56×56 for users who need bigger targets.

     min-height/min-width pinned explicitly because mobile.css line 10
     applies `button { min-height: 36px }` globally for tap targets —
     without these, a 32px height gets stretched to 36px on mobile. */
  width: 32px; height: 32px;
  min-width: 32px; min-height: 32px;
  /* inline-flex + center axes guarantee the inline-SVG icon (icon('x',14)
     macro) sits dead centre regardless of stroke metrics. */
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: var(--black-4);
  border: 1px solid var(--grey-dark);
  border-radius: var(--radius-sm);
  color: var(--grey-text);
  line-height: 1;
  cursor: pointer;
}
/* The inline SVG inside .modal-close has fill="none" + stroke="currentColor"
   per the icon('x', 14) macro shape. Default SVG pointer-events is
   `visiblePainted`, meaning clicks only fire on the 1.6px stroke lines —
   the empty triangles between the X strokes pass clicks through
   unpredictably, and clicks on a <path> child make e.target the path
   element (which delegated handlers using `e.target === button` would
   miss). Setting pointer-events:none on the inner SVG makes every click
   inside the button register on the button itself, so e.target is the
   button and any handler bound via direct addEventListener OR delegated
   .closest('.modal-close') fires reliably. Canonical pattern for
   icon-inside-button across design systems. Caught 2026-05-17 on the
   wizard's class-info overlay (commit 68512fd attempted a JS-side fix
   via .closest() but the root cause was at the CSS layer). */
.modal-close > svg,
.modal-close svg * {
  pointer-events: none;
}
.modal-body {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  padding: 16px 20px;
  -webkit-overflow-scrolling: touch;
}

/* ── Modal footer — the app-wide convention ──
   Every modal that uses the .modal-footer class gets this layout
   automatically: pinned to the bottom of the .dd-modal-card, primary
   actions flush to the right edge with a consistent ~20px gutter and
   proper safe-area-aware bottom padding. Delete (or any destructive
   secondary action) sits LEFT OF Save by flex ordering — no inline
   padding juggling, no spacer divs, no Cancel button (the X in the
   header + Escape + click-outside already dismiss).

   Opt into a different layout with a modifier class:
     .modal-footer.rest-foot     — two equal-width buttons (see overview).

   NEW MODALS: just drop `<div class="modal-footer">…</div>` inside a
   .dd-modal-card form/column and you're done. Don't reinvent the
   inline flex+padding dance; if the layout you need isn't here, add
   a modifier class here first. */
.modal-footer {
  flex: 0 0 auto;
  display: flex;
  gap: 8px;
  align-items: center;
  justify-content: flex-end;
  padding: 14px 20px 18px 20px;
  border-top: 1px solid var(--grey-dark);
  background: var(--black-2);
}

/* Scoped safe-area insets for the ONE full-screen modal we have.
   Keep any future full-screen modal opt into this same pattern
   rather than rebaking env() into the shared .modal-* classes. */
.my-turn-modal .modal-header,
.my-turn-modal .modal-head { padding-top: calc(14px + var(--dd-safe-top)); }
.my-turn-modal .modal-body   { padding-bottom: calc(16px + var(--dd-safe-bottom)); }
.my-turn-modal .modal-footer { padding-bottom: calc(18px + var(--dd-safe-bottom)); }
/* Destructive / secondary actions tagged with the `danger` flavour —
   styled red-bright with a subtle border and sit at the right along
   with Save. Usage: <button class="btn btn-ghost btn-danger">Delete</button> */
.btn-danger {
  color: var(--red-bright) !important;
  border-color: var(--red-border) !important;
}
.btn-danger:hover, .btn-danger:focus-visible {
  background: var(--red-deep) !important;
  color: var(--white) !important;
}

/* Two equal-column modifier — used by Rest modal + global confirm modal
   where Cancel and Confirm present a genuine either/or choice (the
   only footer layout where a Cancel button is OK; see CLAUDE.md).
   Grid over flex because one child might be a <form> wrapping its
   button (hidden inputs for POSTs); grid columns ignore form intrinsic
   min-content so the split stays perfectly 50/50. */
.modal-footer.rest-foot {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 8px;
  align-items: stretch;
  justify-content: initial;
}
.modal-footer.rest-foot > form { margin: 0; min-width: 0; }
.modal-footer.rest-foot > .btn,
.modal-footer.rest-foot > form > .btn { width: 100%; }

.turn-section { margin-bottom: 20px; }
.turn-section-title {
  font-family: var(--font-display);
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--red-bright);
  margin-bottom: 8px;
  display: flex;
  align-items: center;
  gap: 8px;
}
.turn-section-title::after { content: ''; flex: 1; height: 1px; background: var(--grey-dark); }

.turn-action {
  background: var(--black-2);
  border: 1px solid var(--grey-dark);
  border-radius: var(--radius);
  padding: 11px 14px;
  margin-bottom: 7px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  cursor: pointer;
  gap: 12px;
}
.turn-action:active { border-color: var(--red); background: var(--red-glow); }
.turn-action-info { flex: 1; min-width: 0; }
.turn-action-name { font-family: var(--font-display); font-size: 15px; font-weight: 600; color: var(--white); }
.turn-action-desc { font-size: 12px; color: var(--grey-text); margin-top: 2px; }
.do-it {
  background: var(--red);
  color: var(--white);
  font-family: var(--font-display);
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  padding: 8px 12px;
  border-radius: var(--radius-sm);
  border: none;
  cursor: pointer;
  flex-shrink: 0;
  min-height: 36px;
}
.do-it:active { background: var(--red-bright); }

/* ---- My Turn — three-zone modal ----
   NOW / CHOOSE / END. Zones are always open — no expand/collapse, no
   numbered progress, no auto-advance. Each zone is a section card
   with a header strip; the CHOOSE zone carries a filter chip bar
   that scopes its rows by category (Action / Bonus / React / Move
   / Free). Origin: 2026-05-16 redesign — prescriptive stepper forced
   players past empty action slots. */
.mt-zone {
  background: var(--black-3);
  border: 1px solid var(--grey-dark);
  border-radius: var(--radius);
  margin-bottom: 12px;
  overflow: hidden;
}
.mt-zone-now    { border-left: 3px solid var(--red); }
.mt-zone-choose { border-left: 3px solid var(--red-bright); }
.mt-zone-end    { border-left: 3px solid var(--grey-mid); }
.mt-zone-header {
  display: flex;
  align-items: baseline;
  gap: 10px;
  padding: 10px 14px;
  border-bottom: 1px solid var(--grey-dark);
  background: var(--black-2);
}
.mt-zone-title {
  font-family: var(--font-display);
  font-size: 14px;
  font-weight: 700;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--white);
}
.mt-zone-sub {
  font-family: var(--font-display);
  font-size: 11px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--grey-text);
}
.mt-zone-body {
  padding: 12px 14px;
  display: flex;
  flex-direction: column;
  gap: 10px;
}
.mt-zone-list {
  display: flex;
  flex-direction: column;
  gap: 10px;
}

/* Filter chip bar (CHOOSE zone). Each chip is a primary section
   selector (Action / Bonus / React / Move / Free / All) — the player's
   first navigation surface inside CHOOSE. Tall icon+label tile, not
   a generic pill: chips stretch evenly across the row, carry a
   line-style SVG icon above their label, and the active chip pops
   on theme-accent background with a glow.

   Chip text MUST be --white when filled per CLAUDE.md selected-pill
   rule; off-state uses --grey-text. Form-element font-family pinned
   because <button> resets to system-ui (CLAUDE.md Form-element
   font-family required scanner). Origin: user feedback 2026-05-16 —
   earlier 32px-tall text-only chips were "buried" and didn't
   communicate they're the section nav, just looked like a filter
   afterthought. */
.mt-filter-bar {
  display: flex;
  gap: 8px;
  padding: 4px 0 10px;
  border-bottom: 1px solid var(--grey-dark);
  margin-bottom: 6px;
}
.mt-filter-chip {
  flex: 1 1 0;
  min-width: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 4px;
  font-family: var(--font-display);
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.10em;
  text-transform: uppercase;
  padding: 10px 4px;
  min-height: 60px;
  background: var(--black-2);
  border: 1px solid var(--grey-dark);
  /* LM-17: rectangle default. */
  border-radius: var(--radius-sm);
  color: var(--grey-text);
  cursor: pointer;
  transition: background var(--transition), border-color var(--transition),
              color var(--transition), box-shadow var(--transition),
              transform var(--transition);
}
.mt-filter-icon {
  /* Inherits stroke color from chip via currentColor — flips white
     when the chip goes active. */
  flex-shrink: 0;
}
.mt-filter-label {
  /* Wraps if a label happens to be longer than chip width on narrow
     viewports; current set ("All / Action / Bonus / React / Move /
     Free") all fit. */
  line-height: 1;
  white-space: nowrap;
}
.mt-filter-chip:hover {
  border-color: var(--red);
  color: var(--white);
  transform: translateY(-1px);
}
.mt-filter-chip.mt-filter-chip-on {
  background: var(--red);
  border-color: var(--red-bright);
  color: var(--white);
  box-shadow: 0 0 0 1px var(--red-border), 0 4px 12px var(--red-glow);
}

/* Filter logic — when a filter chip is active, hide every element
   in the CHOOSE zone whose data-mt-cat doesn't match. Groups and
   rows both carry data-mt-cat; the combat modifier banner and
   target picker have NO data-mt-cat so they stay visible across
   filters. */
.mt-zone-choose.mt-filter-action [data-mt-cat]:not([data-mt-cat="action"]),
.mt-zone-choose.mt-filter-bonus [data-mt-cat]:not([data-mt-cat="bonus"]),
.mt-zone-choose.mt-filter-reaction [data-mt-cat]:not([data-mt-cat="reaction"]),
.mt-zone-choose.mt-filter-move [data-mt-cat]:not([data-mt-cat="move"]),
.mt-zone-choose.mt-filter-free [data-mt-cat]:not([data-mt-cat="free"]) {
  display: none;
}

/* Pinned End My Turn button — bottom of the END zone. Centered,
   moderate width so it reads as the decisive action without
   stretching full-width like an iOS form footer would. */
/* End-of-zone footer row — mirrors .modal-footer's right-justified
   flex pattern. The wrap div returned by buildZoneEnd is a plain
   block (not flex), so the earlier .mt-end-button { align-self:
   center } silently no-op'd and the button fell back to its
   inline-block default at the left edge — user caught the regression
   2026-05-16. Convention is primary-action RIGHT, same as every
   other save-form footer in the app. */
.mt-end-footer {
  display: flex;
  justify-content: flex-end;
  margin-top: 8px;
}
.mt-end-button {
  padding: 10px 24px;
  min-height: 40px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  text-align: center;
  width: auto;
  min-width: 160px;
}
.mt-tip {
  font-size: 12px;
  line-height: 1.55;
  color: var(--grey-text);
  padding: 8px 10px;
  background: var(--black-2);
  border-radius: var(--radius-sm);
}
/* Stop banner for incapacitating-condition / speed-zero states.
   Loud red treatment so it's unmistakably "you can't do this". In
   the three-zone modal, end-of-turn saves live in the END zone one
   scroll down — no inline CTA needed. */
.mt-banner {
  padding: 12px 14px;
  border-radius: var(--radius-sm);
  display: flex;
  flex-direction: column;
  gap: 8px;
}
.mt-banner-stop {
  background: rgba(204, 34, 34, 0.12);
  border: 1px solid var(--red);
  color: var(--white);
}
/* Amber warning variant — encumbrance / concentration-at-risk / any
   "you can still act but pay attention" nudge. Sits between the
   neutral tip block and the red stop banner in intensity. */
.mt-banner-warn {
  background: rgba(233, 145, 34, 0.10);
  border: 1px solid rgba(233, 145, 34, 0.55);
  color: var(--white);
}
.mt-banner-warn .mt-banner-title { color: var(--warn-text); }
.mt-banner-title {
  font-family: var(--font-display);
  font-size: 14px;
  font-weight: 700;
  letter-spacing: 0.08em;
  color: var(--red-bright);
}
.mt-banner-sub {
  font-size: 12px;
  line-height: 1.55;
  color: var(--white-dim);
}
.mt-group {
  display: flex;
  flex-direction: column;
  gap: 6px;
  padding-top: 4px;
}
.mt-group-title {
  font-family: var(--font-display);
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--grey-mid);
  margin-bottom: 2px;
}
/* All rows inside My Turn share this shape — unified padding, radius,
   and border so variants (alert / buff / plain) only differ in their
   left accent stripe + name colour. Keeps the modal from reading as a
   stack of mismatched cards. */
.mt-option {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  padding: 10px 12px;
  background: var(--black-2);
  border: 1px solid var(--grey-dark);
  border-radius: var(--radius-sm);
}
.mt-option-info {
  flex: 1;
  min-width: 0;
}
.mt-option-name {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: 13px;
  color: var(--white);
}
.mt-option-desc {
  font-size: 12px;
  line-height: 1.45;
  color: var(--grey-text);
  margin-top: 3px;
}
.mt-option .do-it { flex-shrink: 0; }
.mt-muted {
  font-size: 12px;
  font-style: italic;
  padding: 6px 2px;
}
/* Alert group — louder ONLY via the heading. The group itself is a
   transparent wrapper so nested rows don't create double-card
   visuals; each row carries its own accent stripe. A small pulsing
   accent dot sits next to the heading so the player's eye still
   lands there first.
   See LM-7 / LM-8 in CLAUDE.md for why afflictions must be visually
   distinct from info rows. */
.mt-group-alert {
  background: transparent;
  border: 0;
  padding: 0;
  margin-top: 4px;
}
.mt-group-alert > .mt-group-title {
  color: var(--red-bright);
  font-size: 11px;
  display: inline-flex;
  align-items: center;
  gap: 8px;
}
.mt-group-alert > .mt-group-title::before {
  content: "";
  width: 6px; height: 6px;
  border-radius: 50%;
  background: var(--red-bright);
  box-shadow: 0 0 0 0 color-mix(in srgb, var(--red) 60%, transparent);
  animation: mt-alert-pulse 1.8s ease-in-out infinite;
}
@keyframes mt-alert-pulse {
  0%, 100% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--red) 55%, transparent); }
  50%      { box-shadow: 0 0 0 5px color-mix(in srgb, var(--red) 0%, transparent); }
}
/* Affliction row — same base shape as .mt-option, with a red left
   accent stripe + red name. Subtle red tint is gone; the stripe +
   coloured name are enough to signal "this is a condition." */
.mt-cond-row {
  border-left: 3px solid var(--red-bright);
}
.mt-cond-row .mt-option-name {
  color: var(--red-bright);
  font-size: 13px;
  letter-spacing: 0.02em;
}
.mt-cond-row .mt-option-desc { color: var(--white-dim); }
/* Buff row — mirror of the affliction row with a green accent. Keeps
   the same base shape as .mt-option for visual consistency. */
.mt-buff-row {
  border-left: 3px solid #3fbf76;
}
.mt-buff-row .mt-option-name { color: #3fbf76; font-size: 13px; }
.mt-buff-row .mt-option-desc { color: var(--white-dim); }

/* Target picker + info card (My Turn CHOOSE zone persistent context).
   Cross-references DM-revealed enemy info with the player's own
   character data — see LM-12. Sits above the filter chip bar so it
   stays visible across all action filters; weapon rows below depend
   on the selected target's distance / cover state. */
.mt-target-picker { width: 100%; margin-bottom: 6px; }
.mt-target-info { display: flex; flex-direction: column; gap: 4px; }
.mt-target-line {
  display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap;
  font-size: 12px; color: var(--white-dim);
}
.mt-target-k {
  font-family: var(--font-display);
  font-size: 10px; font-weight: 700;
  letter-spacing: 0.14em; text-transform: uppercase;
  color: var(--grey-mid);
  min-width: 72px;
}
.mt-target-v { flex: 1 1 auto; min-width: 0; }
.mt-target-unknown .mt-target-v { color: var(--grey-mid); font-style: italic; }
.mt-target-match { color: #86efac; font-weight: 700; }
.mt-target-mismatch { color: var(--grey-mid); }
.mt-target-chips {
  display: flex; flex-wrap: wrap; gap: 4px;
  margin-top: 4px;
}
.mt-target-chip {
  font-family: var(--font-display);
  font-size: 10px; font-weight: 700;
  letter-spacing: 0.04em;
  padding: 3px 8px;
  /* LM-17: text chip defaults to rectangle; was 999px pill. */
  border-radius: var(--radius-sm);
  border: 1px solid transparent;
  white-space: nowrap;
}
.mt-target-chip[data-tier="in"]   { background: rgba(63,191,118,0.14);  color: #86efac; border-color: rgba(63,191,118,0.3); }
.mt-target-chip[data-tier="long"] { background: rgba(251,191,36,0.14);  color: #fcd34d; border-color: rgba(251,191,36,0.3); }
.mt-target-chip[data-tier="out"]  { background: rgba(204,34,34,0.2);    color: var(--red-bright); border-color: var(--red-border); }
.mt-effect-row { gap: 8px; }
.mt-effect-buttons { display: flex; gap: 6px; flex-wrap: wrap; margin-top: 8px; }

/* "Your Turn" live banner — pinned under the search bar on every sheet
   tab when the campaign's initiative tracker lands on this character.
   Loud red-glow treatment, pulsing dot, direct CTA to the walkthrough. */
.mt-live-banner {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 12px 14px;
  background: linear-gradient(90deg, var(--red-deep), var(--black-2) 60%);
  border: 1px solid var(--red);
  border-radius: var(--radius);
  box-shadow: 0 0 0 1px var(--red-border), 0 0 24px var(--red-glow);
  animation: mt-live-pulse 1.8s ease-in-out infinite;
}
.mt-live-banner-pulse {
  width: 12px; height: 12px;
  border-radius: 50%;
  background: var(--red-bright);
  box-shadow: 0 0 0 0 var(--red-bright);
  animation: mt-live-dot 1.2s ease-out infinite;
  flex-shrink: 0;
}
.mt-live-banner-text { flex: 1; min-width: 0; }
.mt-live-banner-title {
  font-family: var(--font-display);
  font-size: 14px;
  font-weight: 800;
  letter-spacing: 0.2em;
  color: var(--white);
}
.mt-live-banner-sub {
  font-size: 12px;
  color: var(--white-dim);
  margin-top: 2px;
}
@keyframes mt-live-pulse {
  0%, 100% { box-shadow: 0 0 0 1px var(--red-border), 0 0 18px var(--red-glow); }
  50%      { box-shadow: 0 0 0 1px var(--red),        0 0 34px var(--red-glow); }
}
@keyframes mt-live-dot {
  0%   { box-shadow: 0 0 0 0 var(--red-bright); }
  70%  { box-shadow: 0 0 0 12px rgba(230,51,51,0); }
  100% { box-shadow: 0 0 0 0 rgba(230,51,51,0); }
}
@media (prefers-reduced-motion: reduce) {
  .mt-live-banner, .mt-live-banner-pulse { animation: none; }
}

/* DM dashboard — per-PC action toolbar appended under each card. */
.dm-pc-actions {
  display: flex;
  flex-direction: column;
  gap: 6px;
  padding: 8px 0 4px;
  border-top: 1px dashed var(--grey-dark);
  margin-top: 8px;
}
.dm-pc-action-row {
  display: flex;
  justify-content: flex-end;
  gap: 6px;
  align-items: center;
  flex-wrap: wrap;
}
.dm-pc-action-label {
  font-family: var(--font-display);
  font-size: 10px;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--grey-text);
  min-width: 34px;
}
.dm-pc-btn {
  padding: 5px 10px;
  min-height: 28px;
  border: 1px solid var(--grey-dark);
  background: var(--black-3);
  color: var(--white-dim);
  font-family: var(--font-display);
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.06em;
  border-radius: var(--radius-sm);
  cursor: pointer;
}
.dm-pc-btn:hover       { border-color: var(--red-border); color: var(--white); }
.dm-pc-btn-dmg:hover   { border-color: var(--red);    color: var(--red-bright); }
.dm-pc-btn-heal:hover  { border-color: #2a7a2a;       color: #86efac; }
/* Filled-button text MUST be --white per CLAUDE.md filled-chip rule. */
.dm-pc-btn-apply       { background: var(--red-deep); color: var(--white); border-color: var(--red-border); }
.dm-pc-cond-select {
  padding: 5px 8px;
  min-height: 28px;
  border: 1px solid var(--grey-dark);
  background: var(--black-3);
  color: var(--white-dim);
  font-family: var(--font-body);
  font-size: 12px;
  border-radius: var(--radius-sm);
  flex: 1;
  min-width: 120px;
}

/* ---- Condition badges ---- */
.condition-list { display: flex; flex-wrap: wrap; gap: 6px; }
.condition-badge {
  font-family: var(--font-display);
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  padding: 4px 8px;
  border-radius: var(--radius-sm);
  background: var(--black-3);
  color: var(--grey-text);
  border: 1px solid var(--grey-dark);
  cursor: pointer;
}
/* Selected-chip text MUST be --white per CLAUDE.md (red-bright on red-deep
   washes out on every non-crimson theme). */
.condition-badge.active { background: var(--red-deep); color: var(--white); border-color: var(--red-border); }

/* ---- Encumbrance ---- */
/* Encumbrance meter — three-zone bar showing Variant Encumbrance
   thresholds (5/10/15 × STR) always visible, with a coloured fill
   on top that indicates the current state. The point is to let
   players see at a glance "how close am I to the next tier" rather
   than doing 5×STR math in their head. */
.enc-meter { display: flex; flex-direction: column; gap: 4px; }
.enc-track {
  position: relative;
  height: 14px;
  display: flex;
  border-radius: 7px;
  overflow: hidden;
  background: var(--black-4);
}
.enc-zone { height: 100%; }
.enc-z-ok    { background: rgba(76, 180, 100, 0.22); }
.enc-z-warn  { background: rgba(200, 170, 50, 0.22); }
.enc-z-heavy { background: rgba(204,  34, 34, 0.22); }
/* Fill — full-width gradient of the three zone BRIGHT colours with
   hard stops at the zone boundaries (33.333 % / 66.666 %), clipped
   via clip-path to reveal only the left `fill_pct` %. The gradient
   stays anchored to the track (not the fill's own width) so as the
   value crosses into a worse zone, the previous zones' colours
   stay put — only the newly-reached portion adopts the next zone's
   colour. See LM-9 in CLAUDE.md for the app-wide sectioned-meter
   pattern this implements. */
.enc-fill {
  position: absolute;
  inset: 0;
  background: linear-gradient(
    to right,
    var(--success)  0%,      var(--success) 33.333%,
    #c8a932 33.333%,  #c8a932 66.666%,
    #cc2222 66.666%,  #cc2222 100%
  );
  /* `inset(top right bottom left)` — reveals only the left portion. */
  clip-path: inset(0 var(--unfilled, 100%) 0 0);
  transition: clip-path 0.3s ease;
  box-shadow: 0 0 8px var(--shadow-soft) inset;
}
/* Over-capacity override — the only case where a single-colour
   fill is correct, because the entire fill really IS in the "over"
   state (exceeded max, nothing else matters). Pulses softly. */
.enc-fill.enc-f-over {
  background: #e63333;
  animation: encOver 1.4s ease-in-out infinite;
}
@keyframes encOver {
  0%, 100% { filter: brightness(1.0); }
  50%      { filter: brightness(1.3); }
}
/* Thin vertical tick between zones — tiny black sliver so the zone
   boundaries read as chiselled even when the colours are faint. */
.enc-tick {
  position: absolute;
  top: 0; bottom: 0;
  width: 1px;
  background: var(--shadow-50);
  pointer-events: none;
}
/* Threshold labels below the bar — 4 stops line up with the zone
   boundaries (0 / 5×STR / 10×STR / 15×STR). */
.enc-scale {
  display: flex;
  justify-content: space-between;
  font-size: 10px;
  color: var(--grey-mid);
  padding: 0 2px;
}
.enc-scale span:first-child { text-align: left; }
.enc-scale span:last-child  { text-align: right; }

/* Status line under the bar — tier label + short effect reminder.
   The colour matches the fill so bar + line read as one unit. */
.enc-status {
  margin-top: 10px;
  padding: 8px 10px;
  border-radius: var(--radius-sm);
  border-left: 3px solid;
  background: rgba(255,255,255,0.02);
  font-size: 12px;
  line-height: 1.45;
}
.enc-status.enc-f-ok    { border-left-color: var(--success); }
.enc-status.enc-f-enc   { border-left-color: #c8a932; }
.enc-status.enc-f-heavy { border-left-color: #cc2222; }
.enc-status.enc-f-over  { border-left-color: #e63333; background: rgba(230, 51, 51, 0.08); }
.enc-status-label {
  display: block;
  font-family: var(--font-display);
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  font-size: 11px;
  margin-bottom: 2px;
}
.enc-status.enc-f-ok    .enc-status-label { color: var(--success-text); }
.enc-status.enc-f-enc   .enc-status-label { color: #e8c84a; }
.enc-status.enc-f-heavy .enc-status-label { color: #ff5a5a; }
.enc-status.enc-f-over  .enc-status-label { color: #ff7070; }
.enc-status-effect { color: var(--grey-text); }

/* ---- Filter chips ---- */
.filter-bar {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin-bottom: 12px;
}
/* Row 2 (property filters: Prepared / Concentration / Ritual) sits
   under row 1 with a slightly tighter top gap so the two rows read
   as paired. */
.filter-bar.filter-bar-row2 {
  margin-top: -4px;
}
.filter-chip {
  font-family: var(--font-display);
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  padding: 6px 10px;
  border-radius: 20px;
  background: var(--black-3);
  color: var(--grey-text);
  border: 1px solid var(--grey-dark);
  cursor: pointer;
  min-height: 32px;
}
.filter-chip.on { background: var(--red); color: var(--white); border-color: var(--red); }

/* ---- Wizard ---- */
.wizard-steps {
  display: flex;
  align-items: center;     /* keep span / a chips on the same baseline */
  gap: 6px;
  padding: 10px 20px;
  border-bottom: 1px solid var(--grey-dark);
  background: var(--black-2);
  overflow-x: auto;
  scrollbar-width: none;
}
.wizard-steps::-webkit-scrollbar { display: none; }
.wizard-step {
  /* inline-flex + center so the chip's text reads dead-centre and
     spans / anchors / divs all render at the same size. Without
     this, anchor chips inherit baseline-aligned line-height while
     spans get block centring, and the strip gains weird vertical
     wobble between visited (a) and not-yet-visited (span) steps. */
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-family: var(--font-display);
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  padding: 6px 10px;
  line-height: 1;
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--grey-mid);
  border: 1px solid var(--grey-dark);
  white-space: nowrap;
}
.wizard-step.active { background: var(--red); color: var(--white); border-color: var(--red); }
.wizard-step.done { border-color: var(--red-border); color: var(--red-bright); }
/* Step chips render as <a> for any step the user has reached so they
   can jump back to fix earlier choices without using the Back button
   one step at a time. Strip default link styling and let the
   .active / .done modifiers handle look. */
a.wizard-step { text-decoration: none; cursor: pointer; }
a.wizard-step:hover { background: var(--red-deep); color: var(--white); border-color: var(--red); }
a.wizard-step.active:hover { background: var(--red-bright); }
/* mobile.css applies `min-height: 36px` to every `a` for tap-target
   compliance, which makes wizard-step anchors taller than the still-
   inline spans for not-yet-reached steps. Pin both to the same
   28px so the chip strip stays a tidy single-row of equal-height
   pills regardless of element. The padding still gives a 36px hit
   area on mobile via touch-action, so accessibility holds. */
a.wizard-step, span.wizard-step { min-height: 28px; }

.wizard-actions {
  display: flex;
  justify-content: flex-end;
  gap: 8px;
  margin-top: 16px;
  padding: 0 20px;
}

/* ---- List choice (wizard) ---- */
.choice-list { display: flex; flex-direction: column; gap: 8px; }
.choice {
  display: block;
  padding: 12px 14px;
  background: var(--black-2);
  border: 1px solid var(--grey-dark);
  border-radius: var(--radius);
  cursor: pointer;
  transition: border-color var(--transition), background var(--transition);
}
.choice:hover { border-color: var(--red-border); }
.choice input[type=radio], .choice input[type=checkbox] { display: none; }
.choice .choice-name { font-family: var(--font-display); font-size: 15px; font-weight: 600; color: var(--white); }
.choice .choice-sub { font-size: 11px; color: var(--grey-text); margin-top: 2px; font-family: var(--font-display); text-transform: uppercase; letter-spacing: 0.06em; }
.choice.selected, input:checked + .choice { border-color: var(--red); background: var(--red-glow); }

/* D-1 plan-to-20 staged-decision tag — small accent chip rendered
   next to a label whose value was pre-filled from c.level_plan. Used
   on both the roadmap page (where staging happens) and the level-up
   form (where staged values pre-fill on the matching level). Per
   LM-17 rectangle not pill. */
.dd-staged-tag {
  display: inline-block;
  margin-left: 6px;
  padding: 1px 6px;
  background: var(--red-deep);
  color: var(--white);
  border-radius: var(--radius-sm);
  font-family: var(--font-display);
  font-size: 9px;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  vertical-align: middle;
}

/* ---- Wizard starting-kit cards ----
   Used by the equipment step's class kit (A/B/C radio cards) and
   the background kit (single checkbox). Visually similar to .choice
   but laid out as a stacked grid (one row per kit on mobile, two
   columns on wider viewports) so the kit summary can wrap onto a
   second line without crowding. The .selected mirror is JS-driven
   from the radio's change event. Per LM-17, rectangles not pills. */
.dd-kit-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: 8px;
}
@media (min-width: 600px) {
  .dd-kit-grid { grid-template-columns: 1fr 1fr; }
}
.dd-kit-card {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  padding: 12px 14px;
  background: var(--black-2);
  border: 1px solid var(--grey-dark);
  border-radius: var(--radius-sm);
  cursor: pointer;
  transition: border-color var(--transition), background var(--transition);
}
.dd-kit-card:hover { border-color: var(--red-border); }
.dd-kit-card input[type=radio],
.dd-kit-card input[type=checkbox] { display: none; }
.dd-kit-card .dd-kit-body { flex: 1; min-width: 0; }
.dd-kit-card .dd-kit-label {
  font-family: var(--font-display);
  font-size: 14px;
  font-weight: 700;
  color: var(--white);
  letter-spacing: 0.04em;
}
.dd-kit-card .dd-kit-summary {
  font-size: 12px;
  color: var(--grey-text);
  margin-top: 4px;
  line-height: 1.4;
}
.dd-kit-card.selected {
  border-color: var(--red);
  background: var(--red-glow);
}

/* Top tab-bar has been removed — navigation is handled by the bottom-nav only. */

/* ---- Currency row ----
   Five cells always in one line. Each cell stacks vertically —
   label, big number, then a tight -/+ row beneath — so the value
   gets the full cell width. "12,480 gp" fits at any viewport width
   instead of getting crushed by side-by-side -/[n]/+. */
.currency-row {
  display: grid;
  grid-template-columns: repeat(5, 1fr);
  gap: 4px;
}
.currency-cell {
  background: var(--black-3);
  border: 1px solid var(--grey-dark);
  border-radius: var(--radius-sm);
  padding: 4px 2px 3px;
  text-align: center;
  min-width: 0;
  position: relative;
}
.currency-cell .label { font-size: 9px; }
.currency-cell .val { font-family: var(--font-mono); font-size: 15px; color: var(--white); }

/* Per-coin identifier — single top-edge accent strip in the metal's
   colour. Keeps the cells in the dark-chrome family (no glows, no
   label colouring) but still lets you pick out gold vs copper at a
   glance without reading the label. */
.currency-cell[data-coin="pp"] { border-top: 3px solid #d4d4db; }  /* platinum */
.currency-cell[data-coin="gp"] { border-top: 3px solid #d9ae3b; }  /* gold     */
.currency-cell[data-coin="ep"] { border-top: 3px solid #b6c4d9; }  /* electrum */
.currency-cell[data-coin="sp"] { border-top: 3px solid #a8a8ad; }  /* silver   */
.currency-cell[data-coin="cp"] { border-top: 3px solid #b87333; }  /* copper   */

/* Press-and-hold feedback on coin buttons — subtle glow matches the
   HP button "pressed" state. */
.coin-btn.pressed,
.coin-btn:active {
  background: var(--red-deep);
  /* Filled-button text MUST be --white per CLAUDE.md filled-chip rule. */
  color: var(--white);
}

/* ---- Override indicator ---- */
.override-mark {
  display: inline-block;
  width: 5px; height: 5px;
  border-radius: 50%;
  background: var(--red-bright);
  margin-left: 4px;
  vertical-align: middle;
}

/* ---- Tooltips ---- */
.tooltip {
  position: absolute;
  background: var(--black-3);
  border: 1px solid var(--red-border);
  border-radius: var(--radius-sm);
  padding: 8px 12px;
  font-size: 12px;
  color: var(--white-dim);
  font-family: var(--font-mono);
  box-shadow: 0 4px 16px var(--shadow-60);
  z-index: 250;
  max-width: 280px;
  pointer-events: none;
}

/* ---- Toast notifications ---- */
.toast-host {
  position: fixed;
  top: calc(56px + env(safe-area-inset-top));
  left: 50%;
  transform: translateX(-50%);
  z-index: 400;
  display: flex;
  flex-direction: column;
  gap: 6px;
  pointer-events: none;
  width: calc(100% - 32px);
  max-width: 440px;
}
.toast {
  background: var(--black-2);
  /* Curved-edge accent removed 2026-06-15 (user request): neutral grey
     border, no theme-accent left stripe and no theme-tinted border. Kind
     differentiation is now the dot + text (+ the crit glow below). Re-add:
     `border: 1px solid var(--red-border); border-left: 3px solid var(--red);`
     and restore the .toast.<kind> border-left-color rules below. */
  border: 1px solid var(--grey-dark);
  color: var(--white);
  border-radius: var(--radius);
  padding: 10px 14px;
  font-family: var(--font-display);
  font-size: 13px;
  font-weight: 600;
  letter-spacing: 0.04em;
  box-shadow: 0 8px 28px var(--shadow-strong);
  pointer-events: auto;
  transform: translateY(-24px);
  opacity: 0;
  transition: transform 0.22s ease, opacity 0.22s ease;
  display: flex;
  align-items: center;
  gap: 10px;
}
.toast.in { transform: translateY(0); opacity: 1; }
/* Every .toast.<kind> rule MUST use a token from the theme-accent
   family (var(--red), var(--red-bright), var(--red-deep)) so the
   border-left and dot retint with the active theme — amber, emerald,
   violet, crimson, etc. all stay visually coherent. Semantic
   distinction (success vs error vs info) is carried by message text
   and dot-glow (error/crit get a red-glow halo; success/info do not),
   NOT by hue.

   Three-strike origin: this rule landed on the third user-reported
   recurrence (2026-05-18 — "toast notifications once again not
   respecting app theme color"). Prior fixes were partial:
     1. 2026-05-05 "Ensure theme colour more strictly applies to
        banner notifications" — moved .toast.success from a hardcoded
        #2a7a2a (dark-only) to var(--success). Tokenized, not
        theme-aware (--success is green in BOTH dark and light blocks
        in colors.css).
     2. 2026-05-07 "Toast Accent Colors" — fixed .toast.crit to use
        var(--red) so crits retint per theme.
     3. 2026-05-18 (this commit) — fixed .toast.success to use
        var(--red-bright) so success retints per theme too.
   Pattern: when N≥2 variants of a kind-aware component use the
   theme accent and one variant uses a semantic token (e.g.
   --success), the outlier eventually surfaces as a user-reported
   theme drift. Fix is to fold the outlier into the theme-accent
   family.

   --success / --success-bright tokens still exist in colors.css and
   power non-toast success indicators (.pu-status.is-ok and
   .pp-photo-status.is-ok); their absence here is deliberate scope. */
/* .toast.<kind> border-left accents removed 2026-06-15 (user request —
   curved-edge accent). The toast border is now uniform neutral grey; kind
   differentiation is the dot + message text (+ the crit glow below). Re-add:
   .toast.success/error { border-left-color: var(--red-bright); }
   .toast.info { border-left-color: var(--red); } */
/* `.toast.crit` is the dice-outcome variant — DIFFERENT from semantic
   .toast.success. A natural-20 / passed-saving-throw / passed-concentration
   isn't an "action completed" event; it's an "event that occurred" with
   theme-narrative emphasis. The user feedback 2026-05-07 "Toast Accent
   Colors" reported that crit rolls were rendering with green (--success,
   semantic) instead of the active theme accent. Fix: crits use the
   theme accent (--red, re-tints per theme) with a glow for distinction
   from normal info toasts, so on amber theme the crit toast reads amber-
   glow, on emerald theme green-glow, on violet violet-glow — never a
   fixed semantic green that fights the active accent. See dice.js +
   dm_dashboard.js concentration-save callsites. */
.toast.crit {
  /* border-left-color accent removed 2026-06-15 (user request); the glow
     halo is kept — it's a shadow, not a curved edge. Re-add the accent:
     border-left-color: var(--red-bright); */
  box-shadow: 0 0 12px var(--red-glow);
}
.toast .tx { flex: 1; min-width: 0; word-break: break-word; }
.toast .tdot {
  width: 8px; height: 8px; border-radius: 50%;
  background: var(--red-bright); flex-shrink: 0;
}
/* .toast.success .tdot inherits the default .toast .tdot background
   (var(--red-bright)) — no override needed; the absence is the rule.
   Per the three-strike toast-theme-token rule above, the dot retints
   with the active theme. error + crit add a glow halo for urgency
   distinction; success stays calm (no glow). */
.toast.error .tdot { background: var(--red-bright); box-shadow: 0 0 8px var(--red); }
.toast.crit .tdot { background: var(--red-bright); box-shadow: 0 0 8px var(--red-bright); }

/* ---- Tap pulse (visible confirmation on any interactive tap) ---- */
@keyframes tap-pulse {
  0% { box-shadow: 0 0 0 0 var(--red-glow); }
  100% { box-shadow: 0 0 0 12px transparent; }
}
.tap-flash { animation: tap-pulse 0.45s ease-out; }

/* ---- Persistent chip strip (sits under tab bar on sheet pages) ---- */
.chip-strip {
  display: flex;
  gap: 6px;
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: none;
  padding: 8px 12px;
  background: var(--black-3);
  border-bottom: 1px solid var(--grey-dark);
}
.chip-strip::-webkit-scrollbar { display: none; }

.chip {
  flex-shrink: 0;
  font-family: var(--font-display);
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  padding: 6px 10px;
  border-radius: 20px;
  border: 1px solid var(--grey-dark);
  background: var(--black-2);
  color: var(--grey-text);
  cursor: pointer;
  transition: all var(--transition);
  min-height: 32px;
  display: inline-flex;
  align-items: center;
  gap: 4px;
  text-decoration: none;
  white-space: nowrap;
}
.chip .chip-val {
  font-family: var(--font-mono);
  color: var(--white);
  letter-spacing: 0;
}
.chip.on {
  background: var(--red-deep);
  border-color: var(--red);
  /* Selected-chip text MUST be --white per CLAUDE.md filled-chip rule. */
  color: var(--white);
}
/* Quick-Action Bar specialty chip states — all four use theme tokens
   so they re-tint per palette. Visual distinction comes from border
   weight + brightness + glow + an indicator dot, NOT from a fixed
   hue. Origin: 2026-05-05 user feedback "Quick action bar pill colors
   not matching app colour theme" — the previous shapes hardcoded
   rgba blue/red/yellow/green that ignored the active theme on every
   non-crimson palette. Mapping below mirrors the prior semantic
   weight (advantage = light/queued, disadvantage = heavy, warn =
   alarming with glow, good = active + ready dot). */
.chip.adv-on {
  background: var(--red);
  border-color: var(--red-bright);
  color: var(--white);
}
.chip.dis-on {
  background: var(--red-deep);
  border-color: var(--red);
  border-width: 2px;
  padding: 5px 9px;  /* compensate for the +1px border so heights stay flush */
  color: var(--white);
}
.chip.warn {
  background: var(--red-deep);
  border-color: var(--red-bright);
  border-width: 2px;
  padding: 5px 9px;
  color: var(--white);
  box-shadow: 0 0 10px var(--red-glow);
}
.chip.good {
  background: var(--red-deep);
  border-color: var(--red);
  color: var(--white);
}
.chip.good::before {
  /* Ready/active indicator — a small filled dot signals "this is on"
     without relying on color contrast (which is now theme-uniform).
     Sized to match .chip-strip's leading-icon visual weight. */
  content: '';
  display: inline-block;
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--white);
  margin-right: 6px;
  flex-shrink: 0;
}

/* ---- Party Health meter — shared by the player Campaign tab and
   the DM dashboard's Party panel. Mirrors the Encumbrance bar
   pattern (LM-9): thin track, rounded ends, dim 22%-alpha tier
   bands underneath, full-width rainbow gradient clipped via
   clip-path. Tier label next to "Party Health:" in the header
   re-colours to match whichever band the total lives in. */
.party-row { display: flex; flex-direction: column; gap: 6px; }
.party-row-head {
  display: flex; justify-content: space-between; align-items: baseline;
  gap: 8px;
}
.party-name {
  font-weight: 700; color: var(--white); font-size: 14px;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.party-row-total .party-name { font-size: 15px; letter-spacing: 0.02em; }
.party-row-total {
  padding-bottom: 12px;
  border-bottom: 1px solid var(--grey-dark);
}
.party-hp-bar-total { height: 16px; }
.party-hp-meter { display: flex; flex-direction: column; gap: 4px; margin-top: 2px; }
.party-hp-track {
  position: relative;
  display: flex;
  height: 14px;
  background: var(--black-4);
  border-radius: 7px;
  overflow: hidden;
}
.party-hp-zone { flex: 1 1 20%; height: 100%; }
.party-hp-z-1 { background: rgba(230, 51, 51, 0.22); }   /* red */
.party-hp-z-2 { background: rgba(230, 104, 51, 0.22); }  /* orange */
.party-hp-z-3 { background: rgba(233, 145, 34, 0.22); }  /* amber */
.party-hp-z-4 { background: rgba(181, 212, 71, 0.22); }  /* yellow-green */
.party-hp-z-5 { background: rgba(63, 191, 118, 0.22); }  /* green */
.party-hp-fill {
  position: absolute;
  inset: 0;
  background: linear-gradient(
    to right,
    #e63333  0%, #e63333 20%,
    #e66833 20%, #e66833 40%,
    #e99122 40%, #e99122 60%,
    #b5d447 60%, #b5d447 80%,
    #3fbf76 80%, #3fbf76 100%
  );
  clip-path: inset(0 var(--unfilled, 0%) 0 0);
  transition: clip-path 0.3s ease;
  box-shadow: 0 0 8px var(--shadow-soft) inset;
}
.party-hp-tick {
  position: absolute;
  top: 0; bottom: 0;
  width: 1px;
  background: var(--shadow-50);
  pointer-events: none;
}
.party-hp-scale {
  display: flex;
  justify-content: space-between;
  font-size: 10px;
  color: var(--grey-mid);
  padding: 0 2px;
}
.party-hp-scale span:first-child { text-align: left; }
.party-hp-scale span:last-child  { text-align: right; }
.party-tier-name {
  margin-left: 4px;
  font-family: var(--font-display);
  font-weight: 700;
  letter-spacing: 0.02em;
}
/* Label colour exactly matches that tier's band in the fill gradient
   so the text reads as "the same object as the current segment." */
.party-tier-name-1 { color: #3fbf76; }  /* 80–100 % band */
.party-tier-name-2 { color: #b5d447; }  /* 60–80 %  band */
.party-tier-name-3 { color: #e99122; }  /* 40–60 %  band */
.party-tier-name-4 { color: #e66833; }  /* 20–40 %  band */
.party-tier-name-5 {
  color: #e63333;                       /* 0–20 %   band */
  animation: party-tier-pulse 1.6s ease-in-out infinite;
}
@keyframes party-tier-pulse {
  0%, 100% { opacity: 1; }
  50%      { opacity: 0.55; }
}
.party-row-self .party-name { color: var(--red-bright); }

/* ---- Crit / fumble styling on roll log entries ----
   Crit highlight tracks the current theme's accent (var(--red)
   family is re-tinted per theme in the theme-token blocks). Fumble
   is theme-invariant grey — it's "generic bad", not a UI accent. */
.roll-entry.crit {
  background:
    radial-gradient(circle at top right,
      color-mix(in srgb, var(--red-bright) 18%, transparent),
      transparent 60%),
    color-mix(in srgb, var(--red) 10%, transparent);
  box-shadow: inset 0 0 0 1px var(--red-border);
}
.roll-entry.fumble {
  background:
    radial-gradient(circle at top right, rgba(120,120,120,0.16), transparent 60%),
    rgba(34,34,34,0.5);
  box-shadow: inset 0 0 0 1px rgba(136,136,136,0.35);
}
.roll-banner {
  display: inline-block;
  font-family: var(--font-display);
  font-size: 9px;
  font-weight: 700;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  padding: 2px 6px;
  /* LM-17: text banner defaults to rectangle; was 999px pill. */
  border-radius: var(--radius-sm);
  margin-left: 6px;
}
.roll-banner.crit {
  background: color-mix(in srgb, var(--red) 25%, transparent);
  color: var(--red-bright);
  border: 1px solid var(--red-border);
}
.roll-banner.fumble { background: rgba(68,68,68,0.4); color: var(--grey-light); border: 1px solid var(--grey-dark); }
@keyframes rollPulse {
  0% { transform: scale(0.995); opacity: 0.7; }
  100% { transform: scale(1); opacity: 1; }
}
.roll-entry.latest { animation: rollPulse 260ms ease-out; }

/* ---- Initiative tracker ---- */
.init-list { display: flex; flex-direction: column; gap: 6px; }
.init-row {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 10px 12px;
  border: 1px solid var(--grey-dark);
  border-left: 3px solid var(--grey-dark);
  border-radius: var(--radius-sm);
  background: var(--black-2);
  min-height: 44px;
}
.init-row.current {
  border-color: var(--red);
  border-left-color: var(--red-bright);
  background: var(--red-glow);
  box-shadow: 0 0 0 1px var(--red-border);
}
.init-row.is-self { border-left-color: var(--success-bright); }
.init-row .init-num {
  font-family: var(--font-mono);
  font-size: 14px;
  font-weight: 700;
  color: var(--white);
  min-width: 32px;
  text-align: center;
}
.init-row .init-name {
  flex: 1;
  font-family: var(--font-display);
  font-size: 14px;
  font-weight: 600;
  color: var(--white-dim);
  letter-spacing: 0.04em;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.init-row.current .init-name { color: var(--white); }
.init-row .init-ctrl {
  background: var(--black-3);
  border: 1px solid var(--grey-dark);
  color: var(--grey-text);
  width: 32px; height: 32px;
  border-radius: var(--radius-sm);
  cursor: pointer;
  font-family: var(--font-display);
  font-size: 12px;
}
.init-row .init-ctrl:active { border-color: var(--red); color: var(--red-bright); }

.init-flow {
  display: flex;
  gap: 6px;
  margin-top: 8px;
  flex-wrap: wrap;
}
.init-flow .btn { flex: 1; min-width: 80px; }

/* ---- Section action row (no title, just buttons) ----
   Used when a section's "header" is just an action (e.g. "+ Add")
   and the page-level title was removed because the nav tab already
   serves as the title. Right-aligns the button(s) with a standard
   margin-bottom so the underlying content starts at the same
   vertical rhythm a .section-title would have produced. */
.section-actions {
  display: flex;
  justify-content: flex-end;
  gap: 6px;
  margin-bottom: 12px;
  min-height: 36px;
  align-items: center;
}

/* ---- Feature category header ----
   The per-category labels inside the Features tab (Class Features,
   Racial Traits, Background, Feats, Other) originally used the
   plain .label class — 11px dim grey — which read as a caption
   and didn't separate categories visually. Upgrade to a proper
   sub-heading: red-bright like .section-title, underlined divider,
   generous top margin so two categories don't run together. */
.feat-group-label {
  font-family: var(--font-display);
  font-size: 12px;
  font-weight: 700;
  letter-spacing: 0.15em;
  text-transform: uppercase;
  color: var(--red-bright);
  margin-top: 24px;
  margin-bottom: 14px;
  padding-bottom: 8px;
  border-bottom: 1px solid var(--grey-dark);
}
/* First category in the section doesn't need the extra top space —
   the section-title above already provides the break. */
.feat-group-label:first-of-type,
.section-title + .feat-group-label,
.card + .feat-group-label {
  /* keep the mt-12 default on the first feat-group-label that
     follows the section-title */
}
.section-title + .feat-group-label {
  margin-top: 12px;
}

/* ---- First feat-group-label as a section-title-equivalent row ----
   Features tab doesn't render an explicit page-level .section-title
   (the tab nav already says "FEATURES"); instead the FIRST feat-
   group-label plays that role and carries the +Add CTA on the right.
   This wrapper draws the underline across the whole row so the label
   on the left + button on the right share one rhythm — same vertical
   footprint as a .section-title would have on every other character-
   sheet tab. */
.feat-group-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  margin-bottom: 14px;
  padding-bottom: 8px;
  border-bottom: 1px solid var(--grey-dark);
}
/* Inline variant of .feat-group-label — strips the standalone
   spacing / underline so the .feat-group-head wrapper governs them
   instead. Applied only to the first category's label. */
.feat-group-label--inline {
  margin: 0;
  padding: 0;
  border: none;
}

/* ---- Feature description paragraph blocks ----
   Each feature card's desc is split into paragraph blocks on
   blank-line boundaries; every block renders as a .feat-para
   with consistent margins so the spacing between blocks looks
   uniform. white-space: pre-line keeps single \n inside a block
   (label / value pairs) on separate lines without needing <br>. */
.feat-desc { margin: 8px 0 0; }
.feat-para {
  font-size: 13px;
  line-height: 1.55;
  white-space: pre-line;
  margin: 0 0 10px;
}
.feat-para:last-child { margin-bottom: 0; }

/* Feature mechanical-benefit bullets (Open5e effects_desc[]). Sits
   below the prose desc on the Features tab card. Tight line-height
   and a compact bullet hanging indent so a feat with 3-4 effects
   still fits in a dense card layout. */
.feat-effects {
  margin: 8px 0 0;
  padding-left: 18px;
  list-style: disc;
  font-size: 13px;
  line-height: 1.5;
  color: var(--white-dim);
}
.feat-effects li {
  margin-bottom: 4px;
}
.feat-effects li:last-child { margin-bottom: 0; }
.feat-effects li::marker {
  color: var(--accent);
}

/* Preview of the same list inside the Add/Edit Feature modal, shown
   after a picker pick so the user sees what will attach on save. */
.feat-effects-preview {
  display: flex;
  flex-direction: column;
  gap: 6px;
  padding: 10px 12px;
  background: var(--black-3);
  border: 1px solid var(--grey-dark);
  border-left: 3px solid var(--accent);
  border-radius: var(--radius-sm);
}
.feat-effects-preview ul {
  margin: 0;
  padding-left: 18px;
  list-style: disc;
  font-size: 12px;
  line-height: 1.45;
  color: var(--white-dim);
}
.feat-effects-preview ul li { margin-bottom: 4px; }
.feat-effects-preview ul li:last-child { margin-bottom: 0; }
.feat-effects-preview ul li::marker { color: var(--accent); }

/* ---- Class-feature module cards ---- */
.cf-module {
  background: var(--black-2);
  border: 1px solid var(--grey-dark);
  border-left: 3px solid var(--red);
  border-radius: var(--radius);
  padding: 12px 14px;
  margin-bottom: 8px;
}
.cf-module .cf-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  margin-bottom: 8px;
}
.cf-module .cf-title {
  font-family: var(--font-display);
  font-size: 14px;
  font-weight: 700;
  letter-spacing: 0.04em;
  color: var(--white);
}
.cf-module .cf-sub {
  font-size: 11px;
  color: var(--grey-text);
  font-family: var(--font-display);
  letter-spacing: 0.08em;
  text-transform: uppercase;
}
.cf-module.active {
  border-left-color: var(--red-bright);
  box-shadow: 0 0 12px rgba(204,34,34,0.18);
}
.cf-module .cf-body {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
}
.cf-module .cf-counter {
  font-family: var(--font-mono);
  font-size: 16px;
  color: var(--red-bright);
  font-weight: 700;
  min-width: 44px;
  text-align: center;
}
.cf-module .cf-note-input {
  margin-top: 8px;
  padding: 8px 10px;
  font-size: 13px;
  width: 100%;
  background: var(--black-3);
  border: 1px solid var(--grey-dark);
  border-radius: var(--radius-sm);
  color: var(--white);
}
.cf-module .cf-note-input:focus { border-color: var(--red); outline: none; }

/* ---- Confirm-state button (two-click destructive) ---- */
.btn.confirming {
  background: var(--red-bright) !important;
  color: var(--white) !important;
  border-color: var(--red-bright) !important;
  animation: confirmPulse 0.9s ease-in-out infinite;
}
@keyframes confirmPulse {
  0%, 100% { box-shadow: 0 0 0 0 rgba(230,51,51,0.4); }
  50% { box-shadow: 0 0 0 6px rgba(230,51,51,0); }
}

/* ---- Death-save pips / Exhaustion pips ---- */
.ds-pip {
  width: 14px; height: 14px;
  border-radius: 50%;
  border: 1.5px solid var(--grey-mid);
  background: transparent;
  display: inline-block;
  flex-shrink: 0;
}
.ds-pip.good { background: var(--success-bright); border-color: var(--success-bright); box-shadow: 0 0 4px var(--success-bright); }
.ds-pip.bad  { background: var(--red-bright); border-color: var(--red-bright); box-shadow: 0 0 4px var(--red); }

/* Death-save card outcome states — coloured border-left + tag chip
   so "stable" / "dying" / "dead" reads at a glance without the player
   counting pips. */
.ds-card {
  border-left: 3px solid var(--grey-dark);
  transition: border-left-color 0.2s ease;
}
.ds-card.ds-dying  { border-left-color: var(--red-bright); }
.ds-card.ds-stable { border-left-color: var(--success-bright); }
.ds-card.ds-dead   { border-left-color: #7a0000; }
.ds-outcome-tag {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: 10px;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  padding: 3px 8px;
  /* LM-17: text tag defaults to rectangle; was 999px pill. */
  border-radius: var(--radius-sm);
  border: 1px solid;
}
.ds-tag-dying  { color: var(--red-bright); border-color: var(--red-border); background: rgba(204,34,34,0.15); }
.ds-tag-stable { color: var(--success-text);          border-color: var(--success);            background: rgba(60,180,90,0.15); }
.ds-tag-dead   { color: #ff6a6a;          border-color: #7a0000;            background: rgba(122,0,0,0.25); }
.ds-row-label {
  font-size: 11px;
  letter-spacing: 0.06em;
  margin-bottom: 4px;
}
.ds-rules {
  border-top: 1px solid var(--grey-dark);
  padding-top: 8px;
}
.ds-outcome-line {
  font-size: 12px;
  font-weight: 600;
  margin-bottom: 6px;
}
.ds-outcome-dying  { color: var(--red-bright); }
.ds-outcome-stable { color: var(--success-text); }
.ds-outcome-dead   { color: #ff6a6a; }
.ds-rule-list {
  display: flex;
  flex-wrap: wrap;
  gap: 4px 14px;
  font-size: 11px;
  color: var(--grey-text);
}
.ds-rule-list strong {
  color: var(--white-dim);
  font-family: var(--font-mono);
  font-weight: 600;
}

/* ---- Exhaustion meter — color-banded segments + effect stack ---- */
.exh-meter { display: flex; flex-direction: column; gap: 4px; }
.exh-track {
  display: flex;
  gap: 2px;
  height: 14px;
}
.exh-seg {
  flex: 1;
  border-radius: 3px;
  border: 1px solid transparent;
  background: var(--black-4);
  transition: background 0.2s ease, box-shadow 0.2s ease, border-color 0.2s ease;
}
/* Faded tints when the tier is NOT yet reached — hints at upcoming
   severity so the player sees "next step is red" at a glance. */
.exh-seg-1 { background: rgba(217, 168, 73, 0.15); }
.exh-seg-2 { background: rgba(217, 168, 73, 0.15); }
.exh-seg-3 { background: rgba(220, 130, 60, 0.15); }
.exh-seg-4 { background: rgba(220, 130, 60, 0.15); }
.exh-seg-5 { background: rgba(204,  34, 34, 0.18); }
.exh-seg-6 { background: rgba(122,   0,  0, 0.25); }
/* Saturated when reached. */
.exh-seg.on.exh-seg-1 { background: var(--warn); border-color: var(--warn); box-shadow: 0 0 6px rgba(217,168,73,0.4); }
.exh-seg.on.exh-seg-2 { background: var(--warn); border-color: var(--warn); }
.exh-seg.on.exh-seg-3 { background: #dc823c; border-color: #dc823c; box-shadow: 0 0 6px rgba(220,130,60,0.4); }
.exh-seg.on.exh-seg-4 { background: #dc823c; border-color: #dc823c; }
.exh-seg.on.exh-seg-5 { background: #cc2222; border-color: #cc2222; box-shadow: 0 0 8px rgba(204,34,34,0.5); }
.exh-seg.on.exh-seg-6 { background: #7a0000; border-color: #7a0000; box-shadow: 0 0 10px rgba(204,34,34,0.7); }
.exh-scale {
  display: flex;
  gap: 2px;
  font-size: 10px;
  color: var(--grey-mid);
}
.exh-scale span { flex: 1; text-align: center; }

/* Status card under the meter — tier label + stacked effect list. */
.exh-status {
  padding: 10px 12px;
  border-radius: var(--radius-sm);
  border-left: 3px solid;
  background: rgba(255,255,255,0.02);
  font-size: 12px;
  line-height: 1.45;
}
.exh-status-label {
  display: block;
  font-family: var(--font-display);
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  font-size: 11px;
  margin-bottom: 4px;
}
.exh-level-0        { border-left-color: var(--success); }
.exh-level-0 .exh-status-label { color: var(--success-text); }
.exh-level-1        { border-left-color: var(--warn); }
.exh-level-1 .exh-status-label,
.exh-level-2 .exh-status-label { color: #e8c84a; }
.exh-level-2        { border-left-color: var(--warn); }
.exh-level-3        { border-left-color: #dc823c; }
.exh-level-3 .exh-status-label,
.exh-level-4 .exh-status-label { color: #ff9a52; }
.exh-level-4        { border-left-color: #dc823c; }
.exh-level-5        { border-left-color: #cc2222; }
.exh-level-5 .exh-status-label { color: #ff5a5a; }
.exh-level-6        { border-left-color: #7a0000; background: rgba(122,0,0,0.12); }
.exh-level-6 .exh-status-label { color: #ff6a6a; }
.exh-status-effect { color: var(--grey-text); }
.exh-effects {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 3px;
}
.exh-effect-item {
  display: flex;
  align-items: baseline;
  gap: 8px;
  color: var(--grey-text);
  font-size: 12px;
}
.exh-effect-tag {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 18px;
  height: 18px;
  padding: 0 5px;
  border-radius: 3px;
  font-family: var(--font-display);
  font-size: 10px;
  font-weight: 700;
  color: var(--white);
  flex-shrink: 0;
}
.exh-effect-1 .exh-effect-tag,
.exh-effect-2 .exh-effect-tag { background: var(--warn); }
.exh-effect-3 .exh-effect-tag,
.exh-effect-4 .exh-effect-tag { background: #dc823c; }
.exh-effect-5 .exh-effect-tag { background: #cc2222; }
.exh-effect-6 .exh-effect-tag { background: #7a0000; }

/* Concentration banner — break-rule reminder line. */
.conc-rule {
  margin-top: 4px;
  font-size: 11px;
  color: var(--grey-text);
  line-height: 1.4;
}
.conc-rule strong {
  color: var(--white-dim);
  font-family: var(--font-mono);
  font-weight: 600;
}

/* ---- Cantrip scaling hint badge ---- */
.cantrip-scale {
  display: inline-block;
  padding: 3px 8px;
  border-radius: 4px;
  font-family: var(--font-mono);
  font-size: 11px;
  background: rgba(204,34,34,0.14);
  color: var(--red-bright);
  border: 1px solid var(--red-border);
  margin-right: 6px;
}

/* ---- Prepared spells count header ---- */
.prepared-counter {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 10px 14px;
  background: var(--black-3);
  border: 1px solid var(--grey-dark);
  border-left: 3px solid var(--red);
  border-radius: var(--radius-sm);
  margin-bottom: 10px;
}
.prepared-counter.over {
  border-left-color: var(--red-bright);
  background: rgba(204,34,34,0.08);
}
.prepared-counter .pc-label {
  font-family: var(--font-display);
  font-size: 11px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--grey-text);
}
.prepared-counter .pc-count {
  font-family: var(--font-mono);
  font-size: 18px;
  font-weight: 700;
  color: var(--white);
}
.prepared-counter.over .pc-count { color: var(--red-bright); }

/* ---- Running notes ---- */
.note-card {
  background: var(--black-2);
  border: 1px solid var(--grey-dark);
  border-left: 3px solid var(--grey-mid);
  border-radius: var(--radius-sm);
  padding: 10px 12px;
  margin-bottom: 6px;
}
.note-card .note-meta {
  font-family: var(--font-display);
  font-size: 10px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--grey-light);
  margin-bottom: 4px;
  display: flex;
  justify-content: space-between;
  gap: 10px;
}
.note-card .note-text {
  font-size: 13px;
  color: var(--white-dim);
  line-height: 1.5;
  word-break: break-word;
  white-space: pre-wrap;
}

/* Formatted timestamp + "edited" side-note — same small uppercase
   style as .note-meta but letter-spacing toned down on the edited
   suffix so it reads as parenthetical. */
.note-card .note-time { white-space: nowrap; }
.note-card .note-edited {
  text-transform: none;
  letter-spacing: 0;
  font-style: italic;
  color: var(--grey-mid);
}

/* Session + tag chips under the note body. Scoped inside .note-card
   so they don't collide with the top-level .chip strip. All use
   `currentColor` on their SVG per the icon standard. */
.note-card .note-chips {
  display: flex;
  flex-wrap: wrap;
  gap: 4px;
  margin-top: 8px;
}
.note-chip {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 2px 8px;
  font-family: var(--font-display);
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.04em;
  /* LM-17: text chip defaults to rectangle; was 999px pill. */
  border-radius: var(--radius-sm);
  cursor: pointer;
  transition: background 0.15s, border-color 0.15s, color 0.15s;
  background: var(--black-2);
  border: 1px solid var(--grey-dark);
  color: var(--white-dim);
  line-height: 1.4;
}
.note-chip:hover { border-color: var(--accent); color: var(--white); }
.note-chip svg { flex-shrink: 0; }
/* Session chip uses the accent token so it re-tints per theme
   (crimson / emerald / sapphire / etc.). Tag chips stay neutral;
   their type is communicated by the leading icon, not colour, so
   they don't compete with other accent surfaces in the section. */
.note-chip-session {
  border-color: var(--accent);
  color: var(--accent);
}
.note-chip-session:hover {
  background: var(--accent);
  color: var(--white);
  border-color: var(--accent);
}

/* Staged-tag chips under the draft textarea (the ones the user has
   picked + will attach on Add). Distinguishing × hint is built-in
   rather than a separate hover target so tap to remove is obvious. */
.note-draft-tags {
  display: flex;
  flex-wrap: wrap;
  gap: 4px;
  margin: 6px 0 10px;
}
.note-chip-x {
  font-size: 14px;
  line-height: 1;
  margin-left: 2px;
  opacity: 0.7;
}
.note-chip-draft { padding-right: 6px; }

/* "Will tag to current session" breadcrumb next to the Add button —
   quiet but present, same accent treatment as the session chip so
   the relationship reads. */
.note-session-hint {
  display: inline-flex;
  align-items: center;
  font-size: 11px;
  font-family: var(--font-display);
  font-weight: 600;
  letter-spacing: 0.04em;
  color: var(--accent);
  opacity: 0.85;
}

/* Filter bar above the notes list — same .filter-chip atoms as
   elsewhere in the app (spells / levelup). Wraps on mobile so long
   tag names don't force a horizontal scroll. */
.notes-filter-bar {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  padding: 0 2px 10px;
}

/* Tag picker modal body — sectioned by type so Locations / NPCs /
   Quests read as separate groups. The chips themselves reuse the
   app-standard `.filter-chip` so selected state + hover match. */
.tag-pick-group + .tag-pick-group { margin-top: 12px; }
.tag-pick-title {
  font-family: var(--font-display);
  font-size: 11px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--grey-light);
  margin-bottom: 6px;
}
.tag-pick-list {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
}

/* ---- Header icon button (gear etc.) — stays quiet next to the Home btn ---- */
.header-icon-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  /* Shrunk ~10% (36 → 32) to match the top-bar scale-down. min-height
     override needed because mobile.css enforces 36px across every
     <button> / <a> for touch targets; the header cluster gets a custom
     size + still stays above the 24px accessibility floor. */
  width: 32px; height: 32px;
  min-height: 32px;
  border-radius: var(--radius-sm);
  color: var(--grey-text);
  border: 1px solid transparent;
  background: transparent;
  text-decoration: none;
  transition: color var(--transition), border-color var(--transition), background var(--transition);
}
.header-icon-btn > svg {
  /* Icons inside the button shrink with it so stroke weight stays balanced. */
  width: 20px; height: 20px;
}
/* Chip variant — same 32×32 footprint but the host is a theme-accent
   FILLED CIRCLE, matching the .dd-avatar treatment so the Rules + any
   future chip-style chrome reads as a sibling of the profile avatar.
   Color follows the same data-mode rule the avatar family uses:
   #111 silhouette in dark mode (default), #ffffff in light mode (the
   colors.css var(--white) inversion would defeat the override here,
   so the literal hex is intentional — same caveat documented on
   .dd-avatar's data-mode rule). */
.header-icon-btn--chip {
  border-radius: 50%;
  background: var(--red);
  /* White glyph on the theme-accent circle, all modes. (Reverted the
     dark-mode #111 silhouette per user direction 2026-06-11.) */
  color: #ffffff;
}
/* Glyph size match — the bare .header-icon-btn renders its SVG at
   20×20 (a 62.5% fill ratio inside the 32px host). The chip variant
   sits NEXT to the .dd-avatar in the same header cluster; .dd-avatar's
   glyph fills 70% of its 32px host (=22.4px). 2.4px is enough to read
   as "the two chips have different visual weights" even when their
   circles are perfectly aligned. Bump the chip's inner SVG to 70% so
   the sibling pair matches. */
.header-icon-btn--chip > svg {
  width: 70%;
  height: 70%;
}
/* Hover / focus / open-disclosure on EVERY header chip uses the same
   visual affordance — a 2px theme-accent ring around the circle.
   Single rule covers all three chips in the cluster (Rules + Profile
   + Settings page back-arrow) so the user sees one consistent
   interaction language regardless of which one they're pointing at.
   Glyph color stays pinned to the dark/light mode value so the
   accent-contrast doesn't fight against itself. */
.header-icon-btn--chip:hover,
.header-icon-btn--chip:focus-visible,
.header-icon-btn--chip[aria-expanded="true"],
.dd-profile-avatar-trigger:hover,
.dd-profile-avatar-trigger:focus-visible,
.dd-profile-menu[open] > .dd-profile-avatar-trigger {
  color: #ffffff;
  box-shadow: 0 0 0 2px var(--red-border, rgba(255,255,255,0.18));
  /* Drop the avatar's prior border-color flip on open — the ring
     supersedes it. Keeping `border-color: transparent` on the open
     state prevents a double-outline (1px border + 2px box-shadow). */
  border-color: transparent;
}
/* No visible box on hover / click / focus — user-flagged chrome.
   Keep interaction feedback to colour-only so the top-right icons
   stay quiet and never render a red/grey outline box around them.
   This matches the .dd-info-btn convention used in the character
   creator: hover lights the SVG strokes, active flips to accent
   red, no fill / border / outline ever. */
/* Hover + open-disclosure both light the icon in the theme accent.
   Was --white; that's correct for plain anchors but on this control
   class it diverged from sibling <a class="header-icon-btn"> elements
   (Settings) which the global `a:not(.btn):not(.nav-item):hover` rule
   tints theme-red — leaving <button class="header-icon-btn"> (Rules,
   Logout) the only header icons that didn't theme on hover. Switching
   to --red-bright unifies the cluster.

   The [aria-expanded="true"] selector pins theme color on the Rules
   button while its modal is open — standard disclosure pattern, the
   button's open-state mirrors how a nav item's `.active` class shows
   theme color while you're on that page. */
.header-icon-btn:hover,
.header-icon-btn[aria-expanded="true"] { color: var(--red-bright); }
.header-icon-btn:focus { outline: none; border-color: transparent; }
/* Bundle J — keyboard-only focus ring. focus-visible only fires on
   keyboard navigation (not mouse click) so the ring doesn't show
   when a user taps the icon — mouse users see the active flash via
   the :active rule. */
.header-icon-btn:focus-visible {
  outline: 2px solid var(--red-bright);
  outline-offset: 2px;
  border-color: transparent;
}
.header-icon-btn:active { color: var(--red-bright); border-color: transparent; }
.header-icon-btn svg { display: block; }

/* ─── User avatar + account menu ─────────────────────────────────
   Identity affordance added to fix the "where's my username?" beta
   feedback. The avatar is the global "this is you" element: it
   appears in the app header on every page AND as a hero element on
   the More-page identity card. Tapping the header avatar opens a
   small account menu (native <details> — no JS to open/close;
   account_menu.js handles outside-click dismiss only).

   Color: --red is the user's theme accent (themable per-user), so
   the circle naturally reads as "your color." White text matches
   the project's selected-pill rule (--white on accent backgrounds).

   Shape: circle per LM-17 (rectangles default; round is allowed for
   theme-dot / avatar / mascot / pip — avatar is one of the named
   exceptions). */
.dd-avatar {
  /* Size is parameterized by the partial via --avatar-size custom
     property. 32px is the header default; 64px is hero card size. */
  --avatar-size: 32px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: var(--avatar-size);
  height: var(--avatar-size);
  min-width: var(--avatar-size);
  min-height: var(--avatar-size);
  border-radius: 50%;
  background: var(--red);
  /* White glyph on the theme-accent circle, all modes. Literal #fff
     (not var(--white)) because colors.css inverts the chrome tokens in
     light mode — var(--white) resolves to a DARK ink there, defeating
     the intent. Covers BOTH the initial-letter fallback AND the
     iconized child by inheritance (SVG inside uses fill="currentColor").
     Reverted the dark-mode #111 silhouette per user direction 2026-06-11. */
  color: #ffffff;
  font-family: var(--font-display);
  font-weight: 700;
  /* Initial letter scales with circle size: ~50% of diameter reads
     balanced (matches Apple HIG avatar text sizing). clamp guards
     against tiny + comically-huge sizes. */
  font-size: clamp(12px, calc(var(--avatar-size) * 0.5), 36px);
  line-height: 1;
  text-transform: uppercase;
  user-select: none;
  letter-spacing: 0;
  flex-shrink: 0;
}
.dd-avatar-initial {
  /* Inner span exists so font-size scaling can move to here without
     affecting the parent's --avatar-size geometry. Currently empty
     ruleset; structural anchor for future variants (image avatar,
     etc.). */
}
/* Class-icon avatar variant — User.profile_icon resolves to an SVG
   from static/icons/profile-classes/<slug>.svg. The inline SVG inside
   .dd-avatar-icon uses fill="currentColor", so it inherits
   .dd-avatar's color (var(--white) on the theme-accent background) —
   same visual treatment as the initial-letter, just a class glyph
   instead of a single character. Icons by Jime Mosqueda (see
   docs/licenses/dnd-class-icons.md). */
/* Custom-photo variant — the uploaded JPEG FILLS the circle (object-fit
   cover) instead of the 70% glyph inset used by the icon variant. This
   matches every social platform's avatar treatment (Discord, Slack,
   Twitter, etc.) — a user-supplied photo is meant to be its own bezel,
   not a glyph framed inside an accent circle. */
.dd-avatar--photo {
  /* Drop the accent background so a transparent or off-center crop
     doesn't show as a colored ring around the photo. The img itself
     covers 100% of the host so this only matters during the brief
     pre-load flash. */
  background: transparent;
  overflow: hidden;
}
.dd-avatar-photo {
  width: 100%;
  height: 100%;
  display: block;
  border-radius: inherit;
  object-fit: cover;
  object-position: center;
}

.dd-avatar-icon {
  /* Fill the circle minus a small inset so the icon doesn't touch
     the edge — 70% of --avatar-size renders balanced (matches the
     ~50% font-size on the initial-letter variant). */
  width: calc(var(--avatar-size) * 0.7);
  height: calc(var(--avatar-size) * 0.7);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  line-height: 1;
  /* White glyph on the theme-accent fill, all modes (literal #fff per
     the colors.css inversion caveat noted on .dd-avatar). SVGs inside
     use fill="currentColor" so this single color tints every path.
     Reverted the dark-mode #111 silhouette per user direction 2026-06-11. */
  color: #ffffff;
}
.dd-avatar-icon svg {
  /* Stretch the embedded SVG to fill .dd-avatar-icon. The SVG's
     viewBox is already trimmed to the icon glyph (no padding) so
     scaling looks even. */
  width: 100%;
  height: 100%;
  display: block;
  /* Per-icon placement — the three custom properties cascade from
     [data-icon=<slug>] on .dd-avatar (set by _avatar.html when the
     user has chosen an iconized profile). Values seeded by the
     /dev/profile-icons editor and emitted as a <style> block from
     the icon_placement DB table via the profile_icon_placement_style()
     Jinja global. Defaults to identity (no translate, no scale) so
     any icon without an override renders centered at its natural
     size. Percent units on translate resolve against the SVG's own
     size, so the same fraction-values give proportional results at
     56px (picker tile) and 64px (identity card). */
  transform: translate(var(--icon-nudge-x, 0), var(--icon-nudge-y, 0))
             scale(var(--icon-scale, 1));
  transform-origin: 50% 50%;
}
/* HEADER CHIP NEUTRALIZER — the 32px avatar in the header trigger
   sits next to the Rules chip (.header-icon-btn--chip) at the same
   diameter. The per-icon placement nudge benefits the LARGE avatar
   surfaces (56px picker tile, 64px identity card) where the glyph
   centroid drift is visible, but at 32px the same nudge produces a
   vertical offset against the sibling Rules chip whose SVG has no
   transform. Result: the two chips' glyphs sit at different heights
   even though their circles are perfectly aligned. Override to
   identity inside the header trigger so the small-size chip pair
   reads as visually flush. */
.dd-profile-avatar-trigger .dd-avatar-icon svg {
  transform: none;
}

/* ──────────────────────────────────────────────────────────────
   Profile-icon picker grid (user_settings.html "Profile Icon"
   section). 13 SVG tiles + "None" fallback tile. Each tile mirrors
   the .dd-avatar treatment (accent-circle preview) so what-you-see-
   is-what-you-pick. Selected tile gets a theme-accent ring.

   Icons by Jime Mosqueda — credit line lives below the grid in the
   template. See docs/licenses/dnd-class-icons.md. */
.profile-icon-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(80px, 1fr));
  gap: 10px;
  padding-top: 4px;
}
.profile-icon-tile {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
  padding: 8px 4px;
  cursor: pointer;
  border: 1px solid transparent;
  /* LM-17: rectangle (the tile chrome — the inner avatar preview
     stays round per the avatar exception). */
  border-radius: var(--radius-sm);
  transition: background var(--transition), border-color var(--transition),
              transform var(--transition);
}
.profile-icon-tile:hover {
  background: var(--black-2);
  border-color: var(--grey-dark);
  transform: translateY(-1px);
}
.profile-icon-tile.selected {
  background: var(--black-2);
  border-color: var(--red);
  box-shadow: 0 0 0 1px var(--red-border), 0 4px 12px var(--red-glow);
}
.profile-icon-tile input[type="radio"] {
  position: absolute;
  opacity: 0;
  pointer-events: none;
}
.profile-icon-tile-inner {
  /* Match the .dd-avatar circle treatment at 56px — preview shows
     exactly how the avatar will render in the header (32px) +
     hero cards (64px). */
  --avatar-size: 56px;
  width: var(--avatar-size);
  height: var(--avatar-size);
  border-radius: 50%;
  background: var(--red);
  color: var(--white);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
}
.profile-icon-tile-inner svg {
  width: 70%;
  height: 70%;
  display: block;
  /* White glyph on theme-accent fill, all modes — matches .dd-avatar-icon.
     SVG selector only, so the .profile-icon-tile--none tile's
     initial-letter fallback keeps its existing white-on-accent text. */
  color: #ffffff;
  /* Per-icon placement — same cascade pattern as .dd-avatar-icon svg
     above. The three custom properties (--icon-nudge-x / -y / -scale)
     come from [data-icon=<slug>] rules emitted by the
     profile_icon_placement_style() Jinja global, which reads
     static/icons/profile-classes/placements.json — owned by the
     /dev/profile-icons editor. Default identity = centered at natural
     size; tweak via the editor, not by hand-editing this file. */
  transform: translate(var(--icon-nudge-x, 0), var(--icon-nudge-y, 0))
             scale(var(--icon-scale, 1));
  transform-origin: 50% 50%;
}
.profile-icon-tile-initial {
  font-family: var(--font-display);
  font-weight: 700;
  /* Letter is the visual analog of the SVG glyph on the iconized
     tiles — same scale + same theme treatment so the None tile reads
     as a sibling, not a leftover. 36px is ~64% of the 56px circle,
     mirroring the 70% SVG fill the icon tiles use. */
  font-size: 36px;
  line-height: 1;
  text-transform: uppercase;
  /* White letter on theme-accent fill, all modes — same as
     .dd-avatar-icon and .profile-icon-tile-inner svg. Reverted the
     dark-mode #111 silhouette per user direction 2026-06-11. */
  color: #ffffff;
}
.profile-icon-tile-label {
  font-family: var(--font-display);
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.10em;
  text-transform: uppercase;
  color: var(--grey-text);
}
.profile-icon-tile.selected .profile-icon-tile-label {
  color: var(--white);
}
.profile-icon-credit {
  margin-top: 12px;
  padding-top: 10px;
  border-top: 1px solid var(--grey-dark);
  font-size: 11px;
  color: var(--grey-text);
  text-align: center;
}
.profile-icon-credit a {
  color: var(--red-bright);
  text-decoration: none;
}
.profile-icon-credit a:hover {
  text-decoration: underline;
}

/* Profile menu — the canonical identity dropdown in the app header.
   Rendered by templates/_profile_button.html (which is included in
   BOTH base.html header_right AND sheet/_sheet_base.html header_right).
   Per the user's "two buttons in the top right" simplification, the
   only items in the header chrome on every page are [Profile][Rules];
   Logout / Settings / Profile page link all live INSIDE the panel.

   Three-iteration history (so future-Claude doesn't loop):
   v1 (7f6d82b — REVERTED): avatar-only dropdown. Bundled Logout +
     Settings behind a click; gained nothing. Sheet pages didn't show
     it at all because _sheet_base.html overrides header_right.
   v2 (81ecf23 — REVERTED): always-visible @handle chip alongside
     restored Logout / Rules / Settings buttons. On mobile the chip
     text hid via media query → just another circle in a crowded
     header; on a 375px phone the header was full of competing icons.
   v3 (this iteration): two buttons only [Profile][Rules]. Profile
     opens a panel showing @username + Settings + Logout. Mobile-
     friendly because the header chrome is finally minimal AND the
     panel is wide enough to show the @handle as proper text. */
.dd-profile-menu {
  position: relative;
  /* inline-flex + height: 32px collapses the wrapper to match the
     summary trigger inside it. Previously this was inline-block,
     which inherited the 22.5px body line-height and gave the
     wrapper an effective height of ~34.2px — taller than the
     sibling .header-icon-btn (32px). In a flex row with
     align-items: center, the row stretched to 34.2 and the
     wrapper's summary sat at line position 0 (top) instead of
     centered, leaving the avatar circle ~1.1px higher than the
     Rules chip. Locking the wrapper to 32px + flex-centering the
     summary inside puts the avatar at the same baseline as every
     other .header-icon-btn in the cluster. */
  display: inline-flex;
  align-items: center;
  justify-content: center;
  height: 32px;
}
.dd-profile-avatar-trigger {
  /* The avatar IS the summary — no extra padding around it.
     list-style:none strips the native disclosure triangle.
     Match the 32px geometry of sibling .header-icon-btn so the
     [Profile][Rules] pair sits at equal visual weight. */
  list-style: none;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  border: 1px solid transparent;
  border-radius: 50%;
  transition: border-color var(--transition);
}
.dd-profile-avatar-trigger::-webkit-details-marker {
  display: none;
}
/* Hover / focus / open-state visuals for .dd-profile-avatar-trigger
   are unified with the .header-icon-btn--chip ring rule above (search
   for "Hover / focus / open-disclosure on EVERY header chip"). The
   previous per-trigger rules (outline on focus-visible, border-color
   flip on open) are intentionally removed — the unified ring covers
   both cases AND keeps the cluster's interaction language consistent. */
.dd-profile-avatar-trigger:focus { outline: none; }

.dd-profile-menu-content {
  /* Positioned popover anchored under the avatar trigger. z-index
     above sticky chrome (1100) but below modal layer (1200+).
     Matches the .dd-select-popup convention used elsewhere in the
     codebase for header-anchored popovers. */
  position: absolute;
  top: calc(100% + 6px);
  right: 0;
  z-index: 1100;
  /* Compact, content-proportioned panel. Capped width so it reads like a
     standard app profile menu (Gmail / GitHub sit ~260-300px), not a
     half-screen slab — the over-wide panel (driven by the un-truncated
     email below) was the "awkward, text uses too little of it" report.
     max-width keeps it fully on-screen on narrow phones. */
  width: 264px;
  max-width: calc(100vw - 24px);
  background: var(--black-2);
  border: 1px solid var(--grey-dark);
  border-radius: var(--radius);
  box-shadow: 0 8px 24px var(--shadow-medium);
  padding: 4px 0;
  /* Default hidden — the UA stylesheet hides non-<summary> children
     of <details> via display:none, but the explicit `display: flex`
     here would override it. Pin display: none manually + show on
     [open]. (This trip-step bit v1 of the dropdown work; pinning
     manually is the only reliable way.) */
  display: none;
  flex-direction: column;
}
.dd-profile-menu[open] > .dd-profile-menu-content {
  display: flex;
}

/* Identity header inside the panel — avatar + @username + email,
   pinned at top so the user sees who they're logged in as the
   moment the panel opens. This is the actual "always visible
   identity" fix: it's two taps deeper than v2's chip, but the @handle
   appears as soon as the panel opens, with no risk of being squeezed
   off-screen by the header layout. */
.dd-profile-menu-header {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 12px 14px;
  border-bottom: 1px solid var(--grey-dark);
  margin-bottom: 4px;
}
.dd-profile-menu-id {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 2px;
}
.dd-profile-menu-username {
  font-family: var(--font-mono);
  font-size: 13px;
  font-weight: 600;
  color: var(--white);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  line-height: 1.2;
}
.dd-profile-menu-email {
  font-family: var(--font-body);
  font-size: 11px;
  color: var(--grey-text);
  /* Truncate rather than wrap/break — a long email must not widen the
     panel (it was forcing ~440px). The id column is flex:1 min-width:0
     so the ellipsis kicks in at the panel's fixed width. */
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* Menu items — match the .data-row tappable feel from /more so the
   panel reads as part of the project's vocabulary, not a one-off. */
.dd-profile-menu-item {
  display: block;
  padding: 11px 14px;
  font-family: var(--font-body);
  font-size: 13px;
  color: var(--white);
  background: transparent;
  border: 0;
  border-radius: 0;
  text-align: left;
  text-decoration: none;
  cursor: pointer;
  width: 100%;
  transition: background var(--transition), color var(--transition);
}
.dd-profile-menu-item:hover {
  background: var(--black-3);
  color: var(--red-bright);
}
.dd-profile-menu-item:focus { outline: none; }
.dd-profile-menu-item:focus-visible {
  outline: 2px solid var(--red-bright);
  outline-offset: -2px;
}
/* "Pick a username" accent variant — surfaced for users without a
   username set. Theme-bright so the call-to-action is unmistakable
   above the regular items. */
.dd-profile-menu-item-accent {
  color: var(--red-bright);
  font-weight: 600;
}
/* Logout subtle differentiator — destructive-ish action, sits last,
   gets a top border so it visually separates from the navigation
   items. */
.dd-profile-menu-item-logout {
  border-top: 1px solid var(--grey-dark);
  margin-top: 4px;
}
/* Inline form wrapping the logout button — strip the form's own
   layout so the button slots into the menu like every other item. */
.dd-profile-menu-form {
  margin: 0;
}

/* ─── Identity card (More-page hero) ─────────────────────────────
   The "this is you" card at the top of /more. Pairs a 64px avatar
   with @username + theme + character/campaign counts so the page
   answers "where am I?" the moment it opens. Tapping the card
   takes you to /social/pick-username to edit. */
.dd-identity-card {
  display: flex;
  align-items: center;
  gap: 16px;
  padding: 16px;
  background: var(--black-2);
  border: 1px solid var(--grey-dark);
  border-top: var(--card-accent-top); /* curved-edge accent — OFF; re-enable via --card-accent-top in colors.css */
  border-radius: var(--radius);
  text-decoration: none;
  color: var(--white);
  transition: border-color var(--transition), background var(--transition);
}
.dd-identity-card:hover {
  border-color: var(--red-bright);
  background: var(--black-3);
}
.dd-identity-card-body {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 2px;
}
.dd-identity-card-username {
  font-family: var(--font-mono);
  font-size: 18px;
  font-weight: 600;
  color: var(--white);
  word-break: break-all;
  line-height: 1.2;
}
.dd-identity-card-meta {
  font-family: var(--font-body);
  font-size: 12px;
  color: var(--grey-text);
  letter-spacing: 0.04em;
}
.dd-identity-card-edit {
  font-family: var(--font-body);
  font-size: 11px;
  color: var(--red-bright);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  flex-shrink: 0;
}

/* ---- Auth (login / register) ---- */
.auth-card {
  background: var(--black-2);
  border: 1px solid var(--grey-dark);
  border-top: var(--card-accent-top); /* curved-edge accent — OFF; re-enable via --card-accent-top in colors.css */
  border-radius: var(--radius);
  padding: 20px;
  max-width: 420px;
  margin: 20px auto;
}
.auth-title {
  font-family: var(--font-display);
  font-size: 20px;
  font-weight: 700;
  letter-spacing: 0.06em;
  color: var(--white);
  margin-bottom: 16px;
}
.auth-hint {
  font-family: var(--font-body);
  font-size: 12px;
  color: var(--grey-text);
  line-height: 1.55;
  padding: 10px 12px;
  border-left: 3px solid var(--red);
  background: var(--red-glow);
  border-radius: var(--radius-sm);
  margin-bottom: 14px;
}
.auth-remember {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 13px;
  color: var(--white-dim);
  margin: 10px 0 16px;
  cursor: pointer;
}
.auth-remember input { width: 16px; height: 16px; accent-color: var(--red); }
.auth-alt {
  margin-top: 14px;
  font-size: 12px;
  color: var(--grey-text);
  text-align: center;
}
.auth-alt a { color: var(--red-bright); }

/* ---- Header user menu ---- */
.user-menu {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 11px;
  color: var(--grey-text);
}
.user-menu .user-email {
  font-family: var(--font-mono);
  color: var(--white-dim);
  max-width: 140px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* ---- Install hint banner (mobile Safari, pre-standalone) ---- */
.install-hint {
  position: fixed;
  left: 10px; right: 10px;
  bottom: calc(80px + env(safe-area-inset-bottom));
  background: var(--black-2);
  border: 1px solid var(--red-border);
  border-left: 3px solid var(--red);
  border-radius: var(--radius);
  padding: 10px 12px 10px 14px;
  z-index: 58;
  display: flex;
  align-items: center;
  gap: 8px;
  box-shadow: 0 8px 28px var(--shadow-strong);
  max-width: 460px;
  margin: 0 auto;
  font-size: 12px;
  color: var(--white-dim);
  line-height: 1.45;
}
.install-hint-body { flex: 1; min-width: 0; }
.install-hint .mono { color: var(--red-bright); font-family: var(--font-mono); font-size: 11px; }
.install-hint strong { color: var(--white); font-family: var(--font-display); letter-spacing: 0.05em; }
.install-hint-close {
  background: transparent;
  border: 1px solid var(--grey-dark);
  color: var(--grey-text);
  width: 30px; height: 30px;
  border-radius: var(--radius-sm);
  cursor: pointer;
  flex-shrink: 0;
  font-size: 14px;
}
.install-hint-close:active { color: var(--red-bright); border-color: var(--red); }

/* ---- Install promo banner (auth pages — login + register) ----
   Separate from .install-hint above because auth pages don't extend
   base.html (no Jinja icon macros, no pwa.js bootstrap), so they need
   a standalone banner that styles + behaves independently. Same
   theme-accent palette so the two banners feel like one design
   language. Static positioning (not fixed) because auth pages have a
   single section and the banner sits above it as a card-style
   prompt — fixed positioning would compete with the on-screen
   keyboard when the email/password inputs are focused on mobile.
   Injected by static/js/install_promo.js when iOS Safari is detected
   AND the user is not already running as an installed PWA AND has
   not dismissed (session-scoped OR legacy localStorage key from
   pwa.js, so dismissing on either surface covers both). */
.dd-install-promo {
  display: flex;
  align-items: stretch;
  gap: 8px;
  background: var(--black-2);
  border: 1px solid var(--red-border);
  border-left: 3px solid var(--red);
  border-radius: var(--radius);
  padding: 12px 14px;
  margin: 12px auto 0;
  max-width: 420px;
  box-shadow: 0 4px 18px var(--shadow-strong);
}
.dd-install-promo-body { flex: 1; min-width: 0; }
.dd-install-promo-title {
  font-family: var(--font-display);
  font-size: 13px;
  font-weight: 700;
  letter-spacing: 0.05em;
  color: var(--white);
  margin-bottom: 4px;
}
.dd-install-promo-steps {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 4px;
  font-family: var(--font-body);
  font-size: 12px;
  color: var(--white-dim);
  line-height: 1.45;
}
.dd-install-promo-steps svg {
  color: var(--red-bright);
  vertical-align: -2px;
  flex-shrink: 0;
}
.dd-install-promo-close {
  background: transparent;
  border: 1px solid var(--grey-dark);
  border-radius: var(--radius-sm);
  color: var(--grey-text);
  width: 30px; height: 30px;
  cursor: pointer;
  flex-shrink: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  font-family: var(--font-display);
}
.dd-install-promo-close:active {
  color: var(--red-bright);
  border-color: var(--red);
}

/* ---- Chip strip — gear + grouped MOD chip ---- */
/* Gear chip: transparent, borderless, grey line-style SVG. Deliberately
   doesn't inherit .chip so it reads as an affordance rather than a
   fake chip competing with HP/AC/etc. */
.chip-gear {
  background: transparent;
  border: none;
  /* Real padding on both sides so the tap target is a full 32×32
     square — previously the button had `margin-left: -4px` and no
     internal left padding, which pinned the icon against the
     strip's edge and made it finicky to hit on mobile. */
  padding: 4px 8px;
  color: var(--grey-text);
  cursor: pointer;
  flex-shrink: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 32px;
  min-height: 32px;
  transition: color var(--transition);
  -webkit-appearance: none;
  appearance: none;
}
.chip-gear:hover { color: var(--white); }
/* Bundle J — keyboard-only focus needs a visible ring. */
.chip-gear:focus-visible {
  color: var(--white);
  outline: 2px solid var(--red-bright);
  outline-offset: 2px;
}
.chip-gear:active { color: var(--red-bright); }
.chip-gear svg { display: block; }

/* MOD chip groups its label and ± buttons inside one rounded pill so
   the +/- buttons visually belong to the modifier, not the strip. */
.chip-mod {
  padding: 0;
  display: inline-flex;
  align-items: stretch;
  gap: 0;
  overflow: hidden;
  cursor: default;
}
/* Two-line stacked label — 'MOD' on top, value ('+0'/'-2') below —
   so the pill reads at a size you can scan even on a phone. */
.chip-mod-label {
  padding: 4px 10px;
  display: inline-flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 1px;
  line-height: 1;
}
.chip-mod-label .chip-val {
  font-size: 14px;
}
.chip-mod-btn {
  /* inline-flex + center keeps '-' and '+' sitting on the same optical
     centre regardless of the glyphs' differing metrics. Line-height:1
     + padding:0 prevents the text line-box from adding slack that
     would push them off-centre. */
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: transparent;
  border: none;
  border-left: 1px solid var(--grey-dark);
  color: var(--red-bright);
  font-family: var(--font-display);
  font-size: 18px;
  font-weight: 700;
  line-height: 1;
  padding: 0;
  min-width: 36px;
  cursor: pointer;
  transition: background var(--transition);
}
.chip-mod-btn:first-child {
  border-left: none;
  border-right: 1px solid var(--grey-dark);
}
.chip-mod-btn:active { background: var(--red-deep); color: var(--white); }

/* ---- Chip-strip settings modal ----
   Uses the safe-area framework above (--dd-vh, --dd-safe-*).
   See top-of-file documentation for the rules. */
.chip-settings-modal {
  position: fixed;
  inset: 0;
  background: var(--overlay-modal);
  z-index: 320;
  display: none;
  align-items: center;
  justify-content: center;
  /* Safe-area-aware padding so the card never bottoms out into the
     home indicator on iPhone X+ or under iOS Safari's bottom toolbar. */
  padding:
    calc(var(--dd-modal-pad) + var(--dd-safe-top))
    calc(12px + var(--dd-safe-right))
    calc(var(--dd-modal-pad) + var(--dd-safe-bottom))
    calc(12px + var(--dd-safe-left));
  -webkit-overflow-scrolling: touch;
}
.chip-settings-modal.open { display: flex; }
.chip-settings-card {
  width: 100%;
  max-width: 520px;
  background: var(--black-2);
  border: 1px solid var(--red-border);
  border-radius: var(--radius);
  display: flex;
  flex-direction: column;
  /* dvh handles iOS Safari toolbar collapse; subtract overlay padding
     on both sides (so the card never touches the screen edges). */
  max-height: calc(var(--dd-vh) - 2 * var(--dd-modal-pad)
                   - var(--dd-safe-top) - var(--dd-safe-bottom));
}
.chip-settings-head {
  padding: 14px 16px;
  border-bottom: 1px solid var(--grey-dark);
  display: flex;
  align-items: center;
  justify-content: space-between;
}
.chip-settings-title {
  font-family: var(--font-display);
  font-size: 16px;
  font-weight: 700;
  letter-spacing: 0.06em;
  color: var(--white);
}
.chip-settings-body {
  padding: 14px 16px;
  overflow-y: auto;
  flex: 1;
  -webkit-overflow-scrolling: touch;
}
.chip-settings-cat-label {
  font-family: var(--font-display);
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--red-bright);
  margin: 14px 0 6px;
  border-bottom: 1px solid var(--grey-dark);
  padding-bottom: 4px;
}
.chip-settings-cat-label:first-child { margin-top: 0; }
.chip-settings-grid {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
}
.chip-settings-opt {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 8px 12px;
  background: var(--black-3);
  border: 1px solid var(--grey-dark);
  border-radius: 20px;
  cursor: pointer;
  font-size: 12px;
  color: var(--white-dim);
  transition: all var(--transition);
  user-select: none;
}
.chip-settings-opt input { display: none; }
.chip-settings-opt.on {
  background: var(--red-deep);
  border-color: var(--red);
  color: var(--white);
}
.chip-settings-opt:active { transform: scale(0.97); }
.chip-settings-foot {
  padding: 12px 16px;
  border-top: 1px solid var(--grey-dark);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
}

/* THEME OVERRIDES moved to static/css/colors.css. */

/* ---- Search-result deep-link highlight ----
   Applied for ~2.4s when the hash-focus JS scrolls a target into view,
   so the user can visually find what universal search jumped them to. */
/* ─── Hash-focus pulse ──────────────────────────────────────────────
   Theme-aware: outline uses --red-bright so the highlight retints with
   the active palette instead of staying crimson on emerald/violet/etc.
   Outline (not border) because not every target has room for a border
   without layout shift, and outline doesn't take up box space. A light
   white wash underneath makes the pulse readable on any accent colour.
*/
/* Clean color pulse — a soft red glow that ramps in on the target and
   fades out, no outline or expanding offset. Reverted from the
   outline-with-growing-offset (the "oval moving outward") variant per
   product feedback. Uses box-shadow spread rather than outline so the
   halo follows the element's border-radius instead of its bounding box. */
@keyframes ddHighlightPulse {
  0% {
    box-shadow: 0 0 0 0 var(--red-glow);
    background-color: var(--red-glow);
  }
  30% {
    box-shadow: 0 0 22px 4px var(--red);
    background-color: var(--red-glow);
  }
  100% {
    box-shadow: 0 0 0 0 rgba(0, 0, 0, 0);
    background-color: transparent;
  }
}
.dd-highlight {
  animation: ddHighlightPulse 1.4s ease-out 1;
  /* Belt-and-braces for native hash jumps that fire before our JS gets
     to run (pre-boot anchor click, some bfcache restores). Keeps the
     target clear of the sticky chrome without overshooting. */
  scroll-margin-top: 140px;
  scroll-margin-bottom: 80px;
}

/* ---- Footer attribution ---- */
.attr-footer {
  text-align: center;
  padding: 20px;
  font-size: 10px;
  color: var(--grey-mid);
  font-family: var(--font-display);
  letter-spacing: 0.1em;
  text-transform: uppercase;
}

/* ─── Owl messenger peek ─────────────────────────────────────
   One-shot animation that fires when a new campaign chat message
   arrives from someone else. Owl swoops in from the right edge,
   holds briefly, and slides out — total 1 second. Pure CSS
   animation so the JS stays minimal (spawn + remove). Toggleable
   in user settings (localStorage `dd.chat_owl_enabled`).
   `pointer-events: none` so it never eats a click; `will-change`
   promotes to its own layer for smooth transform on mobile.
   iOS keyboard hide-list covered above (LM-3). */
/* Owl messenger — persistent notification anchored top-right.
   Swoops in from off-screen (~0.45s), then idles with a subtle bob
   until the user interacts. Click/tap the owl → navigate to the
   chat (owl element removed on unload); horizontal swipe (touch or
   mouse drag) past threshold → dismiss without navigating. No X
   button, no count badge — the silhouette itself is the signal.
   See LM-3 hide-list for iOS keyboard. */
.dd-owl-peek {
  position: fixed;
  right: 16px;
  /* Sit below the sticky app header + tab nav + chip strip so the
     owl peeks from the top edge without covering the search bar. */
  top: calc(140px + env(safe-area-inset-top, 0px));
  width: 96px;
  height: 96px;
  z-index: 5000;
  pointer-events: auto;   /* clickable + swipeable */
  cursor: pointer;
  opacity: 0;
  transform: translateX(150%);
  will-change: transform, opacity;
  animation: dd-owl-enter 0.45s cubic-bezier(0.25, 0.9, 0.35, 1.12) forwards,
             dd-owl-bob   3.2s ease-in-out 0.5s infinite;
  filter: drop-shadow(0 4px 10px var(--shadow-medium));
  transition: transform 0.18s ease-out, opacity 0.18s ease-out;
  background-image: url("/static/img/owl_messenger.png");
  background-size: contain;
  background-repeat: no-repeat;
  background-position: center;
  /* Disable browser touch panning on the owl so horizontal drag
     becomes a swipe gesture instead of scrolling the page. */
  touch-action: none;
  -webkit-user-select: none;
          user-select: none;
}
.dd-owl-peek.dd-owl-dragging {
  animation: none;        /* pause bob while following finger */
  transition: none;       /* 1:1 follow */
}
.dd-owl-peek.dd-owl-dismissing {
  animation: dd-owl-exit 0.35s cubic-bezier(0.55, 0, 0.8, 0.2) forwards;
  pointer-events: none;   /* can't click during dismissal animation */
}
@keyframes dd-owl-enter {
  0%   { transform: translateX(150%); opacity: 0; }
  75%  { transform: translateX(-8%);  opacity: 1; }  /* slight overshoot */
  100% { transform: translateX(0);    opacity: 1; }
}
@keyframes dd-owl-bob {
  0%, 100% { transform: translate(0, 0); }
  50%      { transform: translate(0, -5px); }
}
@keyframes dd-owl-exit {
  0%   { opacity: 1; }
  100% { transform: translateX(160%); opacity: 0; }
}
/* Desktop: bigger + further from the edge. */
html.dd-desktop .dd-owl-peek {
  width: 120px;
  height: 120px;
  right: 24px;
  top: calc(120px + env(safe-area-inset-top, 0px));
}
/* Accessibility — honour reduced-motion. Drop the bob and the
   entrance overshoot; keep a plain fade-in. Dismiss still animates
   but quicker. */
@media (prefers-reduced-motion: reduce) {
  .dd-owl-peek {
    animation: dd-owl-enter-fade 0.3s ease-in forwards;
    transform: translateX(0);
  }
  @keyframes dd-owl-enter-fade {
    0%   { opacity: 0; }
    100% { opacity: 1; }
  }
}

/* ──────── Tutorial system (see static/js/tutorial.js) ────────
   Three stacked layers:

     .dd-tutorial-scrim   z 10000   full-viewport dim (blocks clicks)
     .dd-tutorial-ring    z 10001   highlight ring over target (no pointer)
     .dd-tutorial-bubble  z 10002   dialogue bubble with host portrait

   The pixel-head PNGs have transparent backgrounds — no avatar frame
   needed. The portrait peeks ABOVE the bubble via negative margin so
   it visually reads as "character speaking", not "icon inside a card."
   Separate ring layer means the spotlighted target's own styles stay
   untouched — no save/restore on element properties. */
.dd-tutorial-root[hidden] { display: none; }

.dd-tutorial-scrim {
  position: fixed;
  inset: 0;
  /* Lighter than a normal modal scrim — users need to SEE the thing
     the ring is pointing at, not just an outline of it. 0.45 keeps
     the background readable while still separating the tutorial
     layer from normal content. */
  background: var(--shadow-45);
  z-index: 10000;
  animation: dd-tut-fade-in 200ms ease-out;
}
.dd-tutorial-ring {
  position: fixed;
  pointer-events: none;
  border: 2px solid var(--red-bright);
  border-radius: 10px;
  box-shadow:
    0 0 0 2px var(--shadow-soft),
    0 0 24px 4px color-mix(in srgb, var(--red-bright) 45%, transparent);
  z-index: 10001;
  transition: top 220ms ease, left 220ms ease,
              width 220ms ease, height 220ms ease;
  display: none;
}
/* LM-1: visibility owned by a class, not inline `display: block`.
   tutorial.js toggles `is-visible` on/off; hideRing() also clears
   the inline top/left/width/height it set so a stale position can't
   leak into the next tour. */
.dd-tutorial-ring.is-visible { display: block; }
.dd-tutorial-bubble {
  position: fixed;
  width: 300px;
  max-width: calc(100vw - 32px);
  background: var(--black-2);
  border: 1px solid var(--grey-dark);
  border-top: var(--card-accent-top); /* curved-edge accent — OFF; re-enable via --card-accent-top in colors.css */
  border-radius: var(--radius);
  padding: 14px 14px 12px;
  box-shadow: 0 14px 40px var(--shadow-strong);
  z-index: 10002;
  animation: dd-tut-bubble-pop 260ms cubic-bezier(.2,.8,.2,1);
}
.dd-tutorial-bubble.dd-tut-bubble-center {
  left: 50% !important;
  top: 50% !important;
  transform: translate(-50%, -50%) !important;
}
/* Desktop gets a wider bubble so prose doesn't crush. */
html.dd-desktop .dd-tutorial-bubble { width: 380px; }

.dd-tut-guide-img {
  display: block;
  width: 96px;
  height: auto;
  /* Negative top margin floats the head above the bubble; negative
     bottom margin pulls the name up under the chin so there's no
     dead whitespace between the head and the label. The pixel-art
     portraits have generous shirt/neck below the face, and without
     the pull-up the name reads disconnected from the character. */
  margin: -88px auto -22px;
  image-rendering: pixelated;
  /* No border, no box — the transparent PNG floats. Drop shadow
     anchors it visually without introducing a frame. */
  filter: drop-shadow(0 6px 8px var(--shadow-medium));
  pointer-events: none;
}
html.dd-desktop .dd-tut-guide-img {
  width: 108px;
  margin: -100px auto -26px;
}

.dd-tut-guide-name {
  font-family: var(--font-display);
  font-size: 15px;
  font-weight: 700;
  color: var(--white);
  text-align: center;
  letter-spacing: 0.04em;
  line-height: 1.1;
}
.dd-tut-guide-role {
  font-family: var(--font-display);
  font-size: 9px;
  font-weight: 700;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--red-bright);
  text-align: center;
  margin: 2px 0 10px;
}
.dd-tut-body {
  font-size: 13px;
  line-height: 1.55;
  color: var(--white-dim);
  margin-bottom: 12px;
  max-height: 38vh;
  overflow-y: auto;
}
.dd-tut-body b, .dd-tut-body strong { color: var(--white); font-weight: 700; }
.dd-tut-body i, .dd-tut-body em { color: var(--white); font-style: italic; }

.dd-tut-footer {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  padding-top: 10px;
  border-top: 1px solid var(--black-4);
}
.dd-tut-progress {
  font-family: var(--font-mono);
  font-size: 10px;
  color: var(--grey-mid);
  letter-spacing: 0.05em;
}
.dd-tut-nav-cluster { display: inline-flex; gap: 6px; }
.dd-tut-btn {
  background: transparent;
  border: 1px solid var(--grey-dark);
  color: var(--grey-text);
  padding: 7px 12px;
  border-radius: var(--radius-sm);
  font-family: var(--font-display);
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  cursor: pointer;
  transition: color var(--transition), background var(--transition),
              border-color var(--transition);
}
.dd-tut-btn:hover:not(:disabled) {
  background: var(--black-3);
  color: var(--white);
  border-color: var(--grey-mid);
}
.dd-tut-btn:disabled { opacity: 0.35; cursor: not-allowed; }
.dd-tut-btn-primary {
  background: var(--red);
  border-color: var(--red);
  color: var(--white);
}
.dd-tut-btn-primary:hover:not(:disabled) {
  background: var(--red-bright);
  border-color: var(--red-bright);
  color: var(--white);
}

/* Tutorial picker (sits in a normal .dd-modal-overlay — card styling
   here is for the tour rows inside. */
.dd-tut-pick-row {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 12px 14px;
  border-bottom: 1px solid var(--black-4);
  cursor: pointer;
  width: 100%;
  background: transparent;
  border-top: none;
  border-left: none;
  border-right: none;
  text-align: left;
  color: inherit;
  font: inherit;
  min-height: 60px;
}
.dd-tut-pick-row:last-child { border-bottom: none; }
.dd-tut-pick-row:hover,
.dd-tut-pick-row:active { background: var(--red-glow); outline: none; }
/* Bundle J — keyboard-only focus needs an outline beyond the
   background tint (background change alone has marginal contrast on
   non-crimson themes where --red-glow is dim). */
.dd-tut-pick-row:focus-visible {
  background: var(--red-glow);
  outline: 2px solid var(--red-bright);
  outline-offset: -2px;
}
.dd-tut-pick-avatar {
  width: 44px;
  height: auto;
  flex-shrink: 0;
  image-rendering: pixelated;
  filter: drop-shadow(0 2px 4px var(--shadow-50));
}
.dd-tut-pick-text { flex: 1; min-width: 0; }
.dd-tut-pick-label {
  font-family: var(--font-display);
  font-size: 14px;
  font-weight: 700;
  color: var(--white);
  letter-spacing: 0.02em;
}
.dd-tut-pick-blurb {
  font-size: 11px;
  color: var(--grey-text);
  margin-top: 2px;
  line-height: 1.35;
}
.dd-tut-pick-chev {
  color: var(--grey-mid);
  flex-shrink: 0;
}

@keyframes dd-tut-fade-in {
  from { opacity: 0; }
  to   { opacity: 1; }
}
@keyframes dd-tut-bubble-pop {
  from { opacity: 0; transform: translateY(8px) scale(0.96); }
  to   { opacity: 1; transform: translateY(0)   scale(1); }
}
.dd-tutorial-bubble.dd-tut-bubble-center {
  animation: dd-tut-bubble-pop-center 260ms cubic-bezier(.2,.8,.2,1);
}
@keyframes dd-tut-bubble-pop-center {
  from { opacity: 0; transform: translate(-50%, -46%) scale(0.96); }
  to   { opacity: 1; transform: translate(-50%, -50%) scale(1); }
}

/* ──────── Roadmap — per-class skill tree ────────
   A branching skill-tree visualisation per class the character has.
   Central trunk runs top to bottom; each level is a "tier" with a
   glowing gate indicator and one or more circular feature nodes
   branching off. Locked nodes desaturate; the trunk itself dims
   below the character's current level.

   Inspired by AAA RPG skill trees (Fallen Order / Path of Exile-
   adjacent). Tap any node — locked or unlocked — for a full info
   card. Lock state is a level gate, never a content gate.

   Layout:
     .rmap-columns      wrapper — stacked mobile, side-by-side desktop.
     .rmap-col          one class column.
     .rmap-tree         vertical-trunk container, per-class.
     .rmap-tier         one level row with gate + nodes.
     .rmap-node         circular feature button.                         */
.rmap-header {
  background: var(--black-2);
  border: 1px solid var(--grey-dark);
  border-left: 3px solid var(--red);
  border-radius: var(--radius);
  padding: 14px 16px;
}
.rmap-header-title {
  display: flex;
  align-items: center;
  gap: 10px;
  font-family: var(--font-display);
  font-size: 20px;
  font-weight: 700;
  color: var(--white);
  letter-spacing: 0.02em;
}
.rmap-header-title .rmap-back {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 32px; height: 32px;
  border-radius: var(--radius-sm);
  color: var(--grey-text);
  text-decoration: none;
  transition: color var(--transition), background var(--transition);
}
.rmap-header-title .rmap-back:hover {
  color: var(--white);
  background: var(--black-3);
}
.rmap-header-blurb {
  margin: 10px 0 0;
  font-size: 13px;
  line-height: 1.5;
  color: var(--white-dim);
}

.rmap-columns {
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.rmap-col-head {
  display: flex;
  align-items: flex-start;
  gap: 12px;
  padding-bottom: 10px;
  border-bottom: 1px solid var(--grey-dark);
  margin-bottom: 12px;
}
.rmap-col-icon {
  flex-shrink: 0;
  width: 40px; height: 40px;
  border: 1px solid var(--red);
  border-radius: var(--radius-sm);
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--red-bright);
  background: var(--red-glow);
}
.rmap-col-icon > svg { width: 24px; height: 24px; }
.rmap-col-meta { flex: 1; min-width: 0; }
.rmap-col-name {
  font-family: var(--font-display);
  font-size: 18px;
  font-weight: 700;
  color: var(--white);
  letter-spacing: 0.02em;
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 8px;
}
.rmap-col-level {
  font-family: var(--font-mono);
  font-size: 11px;
  color: var(--red-bright);
  letter-spacing: 0.1em;
  font-weight: 700;
}
.rmap-col-tagline {
  font-size: 12px;
  color: var(--grey-text);
  margin-top: 2px;
  font-style: italic;
}
.rmap-col-subclass {
  margin-top: 4px;
  display: inline-block;
  font-family: var(--font-display);
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--red-bright);
  background: var(--red-glow);
  padding: 3px 8px;
  border-radius: 10px;
}

/* ── SVG Tree canvas + viewport (frozen-pane layout) ──
   Outer viewport scrolls VERTICALLY — labels + tree move together.
   Inside, a flex row: a fixed-width labels column stays pinned while
   the tree column scrolls HORIZONTALLY on its own. Like Excel's
   frozen-header columns: the user always sees what lane each node
   belongs to, even when scrolled far to the right through the
   level-20 capstone.                                              */
.rmap-viewport {
  position: relative;
  width: 100%;
  max-height: 72vh;
  border: 1px solid var(--grey-dark);
  border-radius: var(--radius);
  background: var(--black);
  overflow-y: auto;
  overflow-x: hidden;
  overscroll-behavior: contain;
  -webkit-overflow-scrolling: touch;
  --rmap-gutter: 170px;
}
html.dd-mobile .rmap-viewport { --rmap-gutter: 110px; }

.rmap-grid {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  min-width: 0;
}

/* Sticky top row — corner spacer + horizontally-scrolling ruler.
   Pins to the top of .rmap-viewport so the level numbers stay
   visible as the user scrolls down through lanes (Excel's frozen
   row header). Horizontal scroll is synced with .rmap-tree-scroll
   via a tiny script in roadmap.html. */
.rmap-head {
  display: flex;
  align-items: stretch;
  position: sticky;
  top: 0;
  z-index: 3;
  background: var(--black);
  border-bottom: 1px solid var(--grey-dark);
  box-shadow: 0 6px 12px var(--shadow-60);
  min-width: 0;
}
.rmap-body {
  display: flex;
  align-items: stretch;
  min-width: 0;
}

.rmap-ruler-scroll {
  flex: 1;
  min-width: 0;
  overflow-x: auto;
  overflow-y: hidden;
  /* Hide the redundant ruler scrollbar — the body's scrollbar is
     the one the user actually interacts with; they stay in sync. */
  scrollbar-width: none;
}
.rmap-ruler-scroll::-webkit-scrollbar { display: none; }
.rmap-ruler-svg { display: block; }

.rmap-labels-col {
  flex-shrink: 0;
  /* width via --rmap-gutter custom prop (inline style on the element). */
  background: var(--black);
  border-right: 1px solid var(--grey-dark);
  box-shadow: 6px 0 12px var(--shadow-60);
  z-index: 2;
  display: flex;
  flex-direction: column;
}
.rmap-labels-corner {
  flex-shrink: 0;
  background: var(--black);
  border-right: 1px solid var(--grey-dark);
}

.rmap-lane-label {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 0 10px;
  flex-shrink: 0;
  background: rgba(255, 255, 255, 0.02);
}
.rmap-lane-label.is-alt { background: rgba(255, 255, 255, 0.04); }

.rmap-lane-label-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  width: 22px;
  height: 22px;
  color: var(--red-bright);
}
.rmap-lane-label-icon > svg {
  width: 22px;
  height: 22px;
  stroke: currentColor;
  fill: none;
  stroke-width: 1.8;
  stroke-linecap: round;
  stroke-linejoin: round;
  display: block;
}

.rmap-lane-label-text {
  font-family: var(--font-display);
  font-size: 12px;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--white);
  line-height: 1.15;
  min-width: 0;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
html.dd-mobile .rmap-lane-label { gap: 5px; padding: 0 6px; }
html.dd-mobile .rmap-lane-label-icon,
html.dd-mobile .rmap-lane-label-icon > svg { width: 16px; height: 16px; }
html.dd-mobile .rmap-lane-label-text {
  font-size: 9px;
  letter-spacing: 0.03em;
  line-height: 1.1;
}

.rmap-tree-scroll {
  flex: 1;
  min-width: 0;   /* lets flex shrink below intrinsic width */
  overflow-x: auto;
  overflow-y: hidden;
  overscroll-behavior-x: contain;
  -webkit-overflow-scrolling: touch;
}
.rmap-tree-svg { display: block; }

/* ── SVG children styling ─────────────────────────────────────── */
.rmap-bg-plate { fill: transparent; }
.rmap-bg-star  { fill: rgba(255, 255, 255, 0.14); }

/* Ruler */
.rmap-ruler-tick {
  stroke: var(--grey-dark);
  stroke-width: 1;
}
.rmap-ruler-num {
  font-family: var(--font-mono);
  font-size: 14px;
  font-weight: 700;
  fill: var(--grey-text);
  text-anchor: middle;
  dominant-baseline: alphabetic;
}
.rmap-ruler-num.is-current { fill: var(--red-bright); font-size: 16px; }
.rmap-ruler-num.is-future  { fill: var(--grey-mid); }

/* Current-level vertical band — subtle accent glow column behind
   every lane so the user can always see "where am I." */
.rmap-current-band {
  fill: color-mix(in srgb, var(--red-bright) 9%, transparent);
}
.rmap-current-line {
  stroke: color-mix(in srgb, var(--red-bright) 70%, transparent);
  stroke-width: 1.5;
  stroke-dasharray: 4 4;
}

/* Lane strips */
.rmap-lane-strip {
  fill: rgba(255, 255, 255, 0.02);
}
.rmap-lane-strip.is-alt {
  fill: rgba(255, 255, 255, 0.04);
}

/* Nodes — circles, with halos + pulses for current-level. */
.rmap-node-g {
  cursor: pointer;
  outline: none;
}
.rmap-node-g:focus-visible .rmap-node-halo {
  stroke: var(--white);
  stroke-width: 2;
}

.rmap-node-pulse {
  fill: none;
  stroke: none;
}
.rmap-node-g.is-current .rmap-node-pulse {
  stroke: color-mix(in srgb, var(--red-bright) 80%, transparent);
  stroke-width: 2;
  fill: none;
  /* Animate the `r` attribute directly instead of using transform:
     scale. `transform-origin` on an SVG element inside a translated
     parent <g> doesn't resolve to the circle's own cx/cy reliably
     across browsers, so the scaled ring drifts off-center. Animating
     r grows from the circle's own centre (cx=0, cy=0 inside the
     translate group) without any transform math. */
  animation: dd-rmap-node-pulse 2.2s ease-out infinite;
}
@keyframes dd-rmap-node-pulse {
  0%   { opacity: 0.8; r: 32; }
  100% { opacity: 0;   r: 52; }
}

.rmap-node-halo {
  fill: none;
  stroke: color-mix(in srgb, var(--red-bright) 28%, transparent);
  stroke-width: 2;
  filter: drop-shadow(0 0 6px color-mix(in srgb, var(--red-bright) 40%, transparent));
  transition: stroke var(--transition);
}
.rmap-node-g.is-locked .rmap-node-halo { stroke: none; filter: none; }

.rmap-node-body {
  fill: var(--red);
  stroke: var(--red-bright);
  stroke-width: 2;
  filter: drop-shadow(0 0 8px color-mix(in srgb, var(--red-bright) 40%, transparent));
  transition: fill var(--transition), stroke var(--transition), transform 150ms;
}
.rmap-node-g:hover .rmap-node-body {
  stroke-width: 3;
  stroke: var(--white);
  filter: drop-shadow(0 0 14px color-mix(in srgb, var(--red-bright) 80%, transparent));
}
.rmap-node-g.is-locked .rmap-node-body {
  fill: var(--black-3);
  stroke: var(--grey-dark);
  filter: none;
}
.rmap-node-g.is-locked:hover .rmap-node-body {
  stroke: var(--grey-mid);
}

.rmap-node-glyph {
  stroke: var(--white);
  color: var(--white);
  fill: none;
  stroke-width: 1.8;
  stroke-linecap: round;
  stroke-linejoin: round;
  pointer-events: none;
  overflow: visible;
}
.rmap-node-g.is-locked .rmap-node-glyph {
  stroke: var(--grey-mid);
  color: var(--grey-mid);
}

.rmap-node-text {
  font-family: var(--font-display);
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.02em;
  fill: var(--white);
  text-anchor: middle;
  dominant-baseline: middle;
}
.rmap-node-g.is-locked .rmap-node-text { fill: var(--grey-mid); }

/* ── Tier row: level gate + branching nodes ── */
.rmap-tier {
  position: relative;
  display: grid;
  grid-template-columns: 56px 1fr;
  column-gap: 14px;
  padding: 10px 0;
  min-height: 80px;
  align-items: center;
}

/* Trunk line running down the left gutter between tier gates. */
.rmap-tier::before {
  content: "";
  position: absolute;
  left: 27px;   /* center of the 56px gutter column */
  top: 0;
  bottom: 0;
  width: 2px;
  background: linear-gradient(
    to bottom,
    color-mix(in srgb, var(--red-bright) 80%, transparent) 0%,
    color-mix(in srgb, var(--red-bright) 80%, transparent) 100%
  );
  box-shadow: 0 0 8px color-mix(in srgb, var(--red-bright) 55%, transparent);
  z-index: 0;
}
.rmap-tier.is-first::before { top: 50%; }
.rmap-tier.is-last::before  { bottom: 50%; }
.rmap-tier.is-locked::before {
  background: var(--grey-dark);
  box-shadow: none;
}

/* ── Level gate: circular medallion with level number ──
   LM-17 exception: the roadmap is rendered as a circular skill-tree
   visual; tier gates, node rings, node icons, and modal icons are
   all designed as circles. Aesthetic call, not an oversight — same
   spirit as theme-dot / dice-pip exceptions. */
.rmap-tier-gate {
  grid-column: 1;
  position: relative;
  z-index: 2;
  width: 44px; height: 44px;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  margin: 0 auto;
  background:
    radial-gradient(circle at 35% 30%, color-mix(in srgb, var(--red-bright) 40%, transparent) 0%, transparent 60%),
    var(--black-1, #050510);
  border: 2px solid var(--red-bright);
  box-shadow:
    0 0 0 3px color-mix(in srgb, var(--red-bright) 15%, transparent),
    0 0 18px color-mix(in srgb, var(--red-bright) 55%, transparent);
}
.rmap-tier-gate-num {
  font-family: var(--font-mono);
  font-size: 14px;
  font-weight: 800;
  color: var(--red-bright);
  text-shadow: 0 0 6px color-mix(in srgb, var(--red-bright) 70%, transparent);
}
.rmap-tier.is-locked .rmap-tier-gate {
  background: var(--black-2);
  border-color: var(--grey-dark);
  box-shadow: none;
}
.rmap-tier.is-locked .rmap-tier-gate-num { color: var(--grey-mid); text-shadow: none; }

/* Current-level gate gets a pulsing ring. */
.rmap-tier.is-current .rmap-tier-gate {
  border-color: var(--red-bright);
  border-width: 3px;
  box-shadow:
    0 0 0 4px color-mix(in srgb, var(--red-bright) 22%, transparent),
    0 0 24px color-mix(in srgb, var(--red-bright) 80%, transparent);
  animation: dd-rmap-gate-pulse 2s ease-in-out infinite;
}
.rmap-tier-gate-ring {
  position: absolute;
  inset: -8px;
  border-radius: 50%;
  border: 1px solid color-mix(in srgb, var(--red-bright) 60%, transparent);
  animation: dd-rmap-gate-ring 2.4s ease-out infinite;
  pointer-events: none;
}
@keyframes dd-rmap-gate-pulse {
  0%, 100% { transform: scale(1); }
  50%      { transform: scale(1.06); }
}
@keyframes dd-rmap-gate-ring {
  0%   { opacity: 0.8; transform: scale(1); }
  100% { opacity: 0;   transform: scale(1.45); }
}

/* ── Node row: branches off the gate to the right ── */
.rmap-tier-nodes {
  grid-column: 2;
  position: relative;
  display: flex;
  flex-wrap: wrap;
  gap: 14px 18px;
  padding: 6px 4px 6px 8px;
  align-items: center;
}

/* Branch line from trunk to the node cluster — a short horizontal
   segment that visually ties the gate to its nodes. Sits behind
   the nodes at z-index 0. */
.rmap-tier-nodes::before {
  content: "";
  position: absolute;
  left: -6px;
  top: 50%;
  width: 20px;
  height: 2px;
  background: color-mix(in srgb, var(--red-bright) 70%, transparent);
  box-shadow: 0 0 6px color-mix(in srgb, var(--red-bright) 45%, transparent);
  transform: translateY(-50%);
  z-index: 0;
}
.rmap-tier.is-locked .rmap-tier-nodes::before {
  background: var(--grey-dark);
  box-shadow: none;
}

/* ── Nodes (circular) ── */
.rmap-node {
  position: relative;
  z-index: 1;
  width: 82px;
  flex-shrink: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 6px;
  padding: 0;
  background: transparent;
  border: none;
  cursor: pointer;
  color: var(--white-dim);
  font-family: var(--font-display);
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.02em;
  text-align: center;
  transition: transform 150ms cubic-bezier(.2,.8,.3,1);
}
.rmap-node:hover, .rmap-node:focus-visible {
  outline: none;
  transform: translateY(-2px);
}
/* Bundle J — keyboard-only focus ring. translateY alone fails as a
   focus indicator for low-vision users; add a box-shadow that wraps
   the node so focus is unmistakable on keyboard nav. */
.rmap-node:focus-visible {
  box-shadow: 0 0 0 2px var(--red-bright);
}
.rmap-node-ring {
  position: absolute;
  inset: 0 auto auto 50%;
  top: 0;
  transform: translateX(-50%);
  width: 58px; height: 58px;
  border-radius: 50%;
  pointer-events: none;
}
.rmap-node-icon {
  position: relative;
  z-index: 1;
  width: 52px; height: 52px;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  background:
    radial-gradient(circle at 35% 30%, color-mix(in srgb, var(--red-bright) 30%, transparent) 0%, transparent 65%),
    var(--black-2);
  border: 2px solid var(--red);
  color: var(--red-bright);
  box-shadow:
    inset 0 0 10px color-mix(in srgb, var(--red-bright) 18%, transparent),
    0 0 12px color-mix(in srgb, var(--red-bright) 35%, transparent);
  transition: transform 150ms, box-shadow 150ms, border-color 150ms;
}
.rmap-node-icon > svg { width: 30px; height: 30px; }
.rmap-node-label {
  line-height: 1.15;
  max-width: 92px;
  color: var(--white-dim);
  transition: color var(--transition);
}
.rmap-node:hover .rmap-node-icon {
  transform: scale(1.07);
  box-shadow:
    inset 0 0 14px color-mix(in srgb, var(--red-bright) 35%, transparent),
    0 0 20px color-mix(in srgb, var(--red-bright) 70%, transparent);
  border-color: var(--red-bright);
}
.rmap-node:hover .rmap-node-label { color: var(--white); }

.rmap-node.is-unlocked .rmap-node-icon {
  background:
    radial-gradient(circle at 35% 30%, color-mix(in srgb, var(--red-bright) 55%, transparent) 0%, transparent 65%),
    var(--red);
  color: var(--white);
  border-color: var(--red-bright);
}
.rmap-node.is-current .rmap-node-ring {
  border: 2px solid color-mix(in srgb, var(--red-bright) 80%, transparent);
  animation: dd-rmap-node-pulse 2.4s ease-out infinite;
}
@keyframes dd-rmap-node-pulse {
  0%   { opacity: 0.9; transform: translateX(-50%) scale(0.9); }
  100% { opacity: 0;   transform: translateX(-50%) scale(1.35); }
}
.rmap-node.is-locked .rmap-node-icon {
  background: var(--black-2);
  color: var(--grey-mid);
  border-color: var(--grey-dark);
  box-shadow: none;
  filter: grayscale(0.7);
  opacity: 0.7;
}
.rmap-node.is-locked .rmap-node-label {
  color: var(--grey-mid);
  opacity: 0.8;
}

/* ── Detail modal ── */
.rmap-modal-card { max-width: 520px; }

.rmap-modal-top {
  display: flex;
  align-items: center;
  gap: 14px;
  margin-bottom: 14px;
  padding-bottom: 12px;
  border-bottom: 1px solid var(--black-4);
}
.rmap-modal-icon {
  flex-shrink: 0;
  width: 62px; height: 62px;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  background:
    radial-gradient(circle at 35% 30%, color-mix(in srgb, var(--red-bright) 55%, transparent) 0%, transparent 65%),
    var(--red);
  color: var(--white);
  border: 2px solid var(--red-bright);
  box-shadow:
    inset 0 0 12px color-mix(in srgb, var(--red-bright) 25%, transparent),
    0 0 18px color-mix(in srgb, var(--red-bright) 40%, transparent);
}
.rmap-modal-icon > svg { width: 34px; height: 34px; }
.rmap-modal-meta { flex: 1; min-width: 0; }
.rmap-modal-state {
  display: inline-block;
  font-family: var(--font-display);
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  padding: 2px 8px;
  border-radius: 10px;
  margin-bottom: 4px;
}
.rmap-modal-state.is-unlocked {
  color: var(--red-bright);
  background: var(--red-glow);
}
.rmap-modal-state.is-locked {
  color: var(--grey-mid);
  background: var(--black-3);
  border: 1px solid var(--grey-dark);
}
.rmap-modal-lvl {
  font-family: var(--font-mono);
  font-size: 14px;
  color: var(--white);
  letter-spacing: 0.08em;
  font-weight: 700;
}
.rmap-modal-source {
  font-size: 11px;
  color: var(--grey-text);
  margin-top: 3px;
  letter-spacing: 0.04em;
  font-style: italic;
}

.rmap-modal-stats {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin-bottom: 14px;
}
.rmap-modal-pill {
  background: var(--black-3);
  border: 1px solid var(--grey-dark);
  border-radius: var(--radius-sm);
  padding: 5px 10px;
  min-width: 64px;
}
.rmap-modal-pill-label {
  font-family: var(--font-display);
  font-size: 8px;
  font-weight: 700;
  letter-spacing: 0.15em;
  text-transform: uppercase;
  color: var(--grey-mid);
}
.rmap-modal-pill-value {
  font-family: var(--font-mono);
  font-size: 12px;
  font-weight: 700;
  color: var(--red-bright);
  margin-top: 2px;
}

.rmap-modal-desc {
  font-size: 13px;
  line-height: 1.6;
  color: var(--white-dim);
}
.rmap-modal-desc b, .rmap-modal-desc strong { color: var(--white); }
.rmap-modal-desc i, .rmap-modal-desc em { color: var(--white); font-style: italic; }

/* ── Desktop widening: multi-class side-by-side ── */
html.dd-desktop .rmap-columns {
  flex-direction: row;
  flex-wrap: wrap;
  gap: 16px;
  align-items: flex-start;
}
html.dd-desktop .rmap-col {
  flex: 1 1 420px;
  min-width: 360px;
  max-width: 640px;
}
html.dd-desktop .rmap-header-title { font-size: 24px; }

/* ── Level-Up ↔ Roadmap tab strip ──
   Sibling pages ("what's next?"): Level Up picks the next level and
   confirms its unlocks; Roadmap shows the full 1→20 progression.
   Tabs render on both pages with the current one flagged active. */
.lu-tabs {
  display: flex;
  gap: 6px;
  padding: 4px;
  margin-bottom: 12px;
  background: var(--black-2);
  border: 1px solid var(--grey-dark);
  border-radius: var(--radius);
}
.lu-tab {
  flex: 1;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  padding: 10px 12px;
  border-radius: var(--radius-sm);
  font-family: var(--font-display);
  font-size: 12px;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--grey-text);
  background: transparent;
  text-decoration: none;
  cursor: pointer;
  transition: color var(--transition), background var(--transition);
}
.lu-tab:hover {
  color: var(--white);
  background: var(--black-3);
}
.lu-tab.is-active {
  color: var(--white);
  background: var(--red);
  cursor: default;
}

/* ======================================================   Bundle H: offline + a11y
   Pain #58 (offline mode V1) + Pain #61 (screen-reader support).
   ============================================================= */

/* Skip-to-main link — visible only when focused via keyboard. The
   off-screen positioning makes it inaudible to mouse users but
   accessible to keyboard / screen-reader users immediately on Tab. */
.skip-link {
  position: absolute;
  left: 0;
  top: -48px;
  z-index: 12000;       /* above sticky chrome (~9000) and splash (10000) */
  padding: 10px 16px;
  background: var(--red);
  color: var(--white);
  font-family: var(--font-display);
  font-size: 14px;
  font-weight: 600;
  text-decoration: none;
  border-radius: 0 0 var(--radius-sm) 0;
  transition: top 0.15s ease-out;
}
.skip-link:focus,
.skip-link:focus-visible {
  top: 0;
  outline: 2px solid var(--white);
  outline-offset: -2px;
}

/* Visually hidden but keyboard-focusable. Use on form inputs that are
   styled-via-label-wrapper (e.g. levelup ASI/HP radios where the chip
   span carries the visible style and the input is invisible). Beats
   `style="display:none;"` because display:none removes the input from
   the focus order entirely — keyboard users couldn't tab to it. The
   classic clip-rect pattern keeps the input rendered (focusable) but
   takes zero visual space. */
.dd-vh {
  position: absolute !important;
  width: 1px !important;
  height: 1px !important;
  padding: 0 !important;
  margin: -1px !important;
  overflow: hidden !important;
  clip: rect(0, 0, 0, 0) !important;
  white-space: nowrap !important;
  border: 0 !important;
}

/* Offline banner — rendered by pwa.js inside the active main content
   area when the SW serves a cached fallback. card / card-pad come
   from base; only the row layout + dismiss button styling is new. */
.dd-offline-banner {
  margin: 0 0 12px;
}
.dd-offline-banner-row {
  display: flex;
  align-items: center;
  gap: 10px;
}
.dd-offline-banner-icon {
  color: var(--grey-mid);
  flex-shrink: 0;
  display: inline-flex;
}
.dd-offline-banner-text {
  flex: 1;
  font-size: 13px;
  color: var(--white-dim);
  line-height: 1.4;
}
.dd-offline-banner-close {
  background: transparent;
  border: none;
  color: var(--grey-mid);
  font-size: 16px;
  width: 28px;
  height: 28px;
  cursor: pointer;
  border-radius: var(--radius-sm);
  display: inline-flex;
  align-items: center;
  justify-content: center;
}
.dd-offline-banner-close:hover {
  color: var(--white);
  background: var(--black-3);
}

/* Bundle N (Pain #58 v2) — offline indicator chip + conflict banner.
   Chip is a top-right rectangle (LM-17) visible only when offline OR
   the queue is non-empty. Banner is a red-bordered card-pad rendered
   inside the active main content area when conflicts need review. */
.dd-offline-chip {
  position: fixed;
  top: 8px;
  right: 8px;
  z-index: 9500;
  background: var(--grey-mid, #555);
  color: var(--white);
  font-family: var(--font-display);
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.02em;
  padding: 5px 10px;
  border-radius: var(--radius-sm);
  box-shadow: 0 2px 6px var(--shadow-35);
  pointer-events: none;
  max-width: 60vw;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.dd-offline-chip[hidden] { display: none; }

.dd-offline-conflict-banner {
  margin: 0 0 12px;
  border: 1px solid var(--red, #c0392b);
  background: rgba(192, 57, 43, 0.08);
}
.dd-offline-conflict-row {
  display: flex;
  align-items: center;
  gap: 10px;
  justify-content: space-between;
}
.dd-offline-conflict-text {
  flex: 1;
  font-size: 13px;
  color: var(--white);
}
.dd-offline-conflict-item {
  padding: 8px 0;
  border-bottom: 1px solid var(--black-4, #222);
}
.dd-offline-conflict-item:last-child { border-bottom: 0; }
.dd-offline-conflict-item-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
}
.dd-offline-conflict-item-label {
  flex: 1;
  font-size: 13px;
  color: var(--white-dim);
  word-break: break-word;
}

/* =======================================================================
   Bundle D: safety + a11y
   Pains #55 (safety tools), #62 (OpenDyslexic font), #64 (motor a11y).

   This block is grouped here so simultaneous bundles editing base.css
   don't collide with existing rules. NEW additions only — no edits to
   anything above.
   ======================================================================= */

/* ── #62: OpenDyslexic font option ──────────────────────────────────────
   OpenDyslexic is a typeface designed for readers with dyslexia
   (heavier-bottom letterforms anchor each glyph). When the user
   opts in via Settings, html.dd-opendyslexic gets added at first
   paint and we override --font-display to point at OpenDyslexic.

   IMPORTANT — LM-2 invariant: the "two fonts only" rule is preserved
   because OpenDyslexic REPLACES Rajdhani for that user — at no point
   are three faces present on a page. Share Tech Mono stays the only
   numeric font (digits matter for alignment; OpenDyslexic's
   tabular-nums coverage isn't sufficient and the variable widths
   would hurt HP / ability score columns). See LM-2 in CLAUDE.md for
   the documented exception.

   We DON'T preconnect OpenDyslexic in base.html — it loads lazily
   from the @font-face below, which means default users never pay
   for a font they won't use. */
@font-face {
  font-family: 'OpenDyslexic';
  src: url('https://cdn.jsdelivr.net/npm/open-dyslexic@1.0.3/woff/OpenDyslexic-Regular.woff') format('woff');
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: 'OpenDyslexic';
  src: url('https://cdn.jsdelivr.net/npm/open-dyslexic@1.0.3/woff/OpenDyslexic-Bold.woff') format('woff');
  font-weight: 700;
  font-style: normal;
  font-display: swap;
}
:root {
  /* Stack: OpenDyslexic primary, Rajdhani fallback (loads instantly
     while OpenDyslexic streams in), system sans-serif as the final
     fallback so the page never reads as a default-serif blob if both
     network requests fail. */
  --font-display-dyslexic: 'OpenDyslexic', 'Rajdhani', system-ui, sans-serif;
}
html.dd-opendyslexic {
  --font-display: var(--font-display-dyslexic);
  /* --font-body aliases --font-display, so it picks up automatically. */
}

/* ── #64: Skip Animations ──────────────────────────────────────────────
   Force every transition + animation to ~0ms. Belt-and-suspenders
   approach — the !important is intentional because individual
   components do `transition: transform 0.18s ease` inline-style and
   we need to defeat all of them. Respects user opt-in only — does
   NOT auto-flip on prefers-reduced-motion (some users actively want
   the dice-roll animations even with motion sensitivity). */
html.dd-no-anim *,
html.dd-no-anim *::before,
html.dd-no-anim *::after {
  animation-duration: 0.001s !important;
  animation-delay:    0s    !important;
  animation-iteration-count: 1 !important;
  transition-duration: 0.001s !important;
  transition-delay:    0s    !important;
}

/* OS-level prefers-reduced-motion — honor for decorative animations
   (chrome fades, hover transitions, modal slide-ins) but PRESERVE
   the dice tray + dice-roll feedback because the dice are a
   first-class part of the experience and the random-feedback timing
   is the whole point. WCAG 2.1 3.2.3 wants reduced-motion honored
   unless the user opts in; the manual "Skip animations" toggle in
   Settings remains the kill-everything override for users who want
   dice silenced too. The opt-out scope is .dd-dice-fab + .dd-dice-overlay
   + the d20-pulse keyframe specifically. */
@media (prefers-reduced-motion: reduce) {
  *:not(.dd-dice-fab):not(.dd-dice-fab *):not(.dd-dice-overlay):not(.dd-dice-overlay *),
  *:not(.dd-dice-fab):not(.dd-dice-fab *):not(.dd-dice-overlay):not(.dd-dice-overlay *)::before,
  *:not(.dd-dice-fab):not(.dd-dice-fab *):not(.dd-dice-overlay):not(.dd-dice-overlay *)::after {
    animation-duration: 0.001s !important;
    transition-duration: 0.001s !important;
  }
}

/* ── #64: Larger Tap Targets ───────────────────────────────────────────
   Bump every interactive surface to a 56×56 minimum (vs the default
   44×44 mobile minimum). The override sits at the bottom so it wins
   the cascade against the per-component sizing. We don't grow
   in-form chips/checkboxes inside dense lists (would reflow every
   sheet tab); we DO grow standalone buttons + theme dots + nav
   items — the controls users hunt for and tap deliberately. */
html.dd-large-taps .btn,
html.dd-large-taps .btn-sm,
html.dd-large-taps .header-icon-btn,
html.dd-large-taps .icon-btn,
html.dd-large-taps .theme-dot,
html.dd-large-taps .bottom-nav a,
html.dd-large-taps .app-nav a,
html.dd-large-taps .modal-close,
html.dd-large-taps .toggle-switch,
html.dd-large-taps button.tappable {
  min-width: 56px;
  min-height: 56px;
}
html.dd-large-taps .btn,
html.dd-large-taps .btn-sm {
  /* Text sizing bumps proportionally so the bigger button isn't
     lost in whitespace. */
  padding: 14px 18px;
  font-size: 14px;
}
html.dd-large-taps .toggle-switch {
  /* Switches are 46×26 by default — scale up to a comfortable
     touch target without distorting the iOS-style proportions. */
  width: 64px;
  height: 36px;
}
html.dd-large-taps .toggle-switch::before {
  width: 30px;
  height: 30px;
}
html.dd-large-taps .toggle-switch:checked::before {
  transform: translateX(28px);
}

/* ── #64: Hold-to-Confirm progress fill ────────────────────────────────
   When the hold-to-confirm pref is on, the confirm modal's primary
   button gains a press-and-hold gesture: tap-and-hold for 600ms to
   fire (vs single-tap). Visual feedback comes from a CSS-only fill
   that animates as the button is held. confirm.js drives the state
   class flip; CSS handles the fill. */
.dd-hold-confirm-btn {
  position: relative;
  overflow: hidden;
}
.dd-hold-confirm-btn::after {
  content: '';
  position: absolute;
  inset: 0;
  background: var(--red-glow);
  transform: scaleX(0);
  transform-origin: left center;
  pointer-events: none;
  transition: transform 0.6s linear;
}
.dd-hold-confirm-btn.is-holding::after {
  transform: scaleX(1);
}

/* ── #55: Safety tools — X-Card button polish ──────────────────────────
   The btn-lg modifier is used by the player Campaign tab's X-Card
   button. Larger padding + min-height for a confident tap target
   on what's a high-stakes pause-the-game action.
   LM-17: still a rectangle (radius-sm), red border via btn-danger,
   never a pill. */
.btn.btn-lg {
  padding: 14px 20px;
  font-size: 15px;
  min-height: 56px;
  letter-spacing: 0.08em;
}

/* ── #55: Safety tools — DM dashboard X-Card alert banner ──────────────
   Banner sits OUTSIDE the dm-panel scope so it never gets hidden
   when the DM switches tabs. Pulse the border once a second so a
   skim across the page registers it; respects dd-no-anim. */
@keyframes dd-xcard-pulse {
  0%   { box-shadow: 0 0 0 0 var(--red-glow); }
  50%  { box-shadow: 0 0 0 6px transparent; }
  100% { box-shadow: 0 0 0 0 var(--red-glow); }
}
#dm_xcard_alert_card {
  animation: dd-xcard-pulse 1.6s ease-in-out infinite;
}

/* ── LM-3: keyboard hide-list addition ─────────────────────────────────
   The X-Card button on the player Campaign tab is INSIDE the page
   flow (not position:fixed bottom), so it doesn't need
   html.dd-kb-open hiding. The X-Card MODAL uses the existing
   .dd-modal-overlay pattern which already accounts for keyboard
   overlap (modals span 100dvh and intentionally cover keyboard
   area). No new fixed-bottom chrome introduced. */


/* ── Pain #51 — Interactive Campaign Maps ───────────────────────────
   DM uploads an image, taps to place rectangle pin markers; players
   view + tap markers, and toggle a per-user "visited" flag that the
   DM sees on next render (LM-21). Marker pins are rectangles per
   LM-17 (round shapes are reserved for theme-dot/avatar/mascot/pip).
*/
.upload-zone {
  border: 1px dashed var(--grey-dark);
  border-radius: var(--radius-sm);
  padding: 18px 14px;
  text-align: center;
  cursor: pointer;
  background: var(--black-3);
  color: var(--white-dim);
  transition: border-color 0.15s, background 0.15s;
}
.upload-zone:hover, .upload-zone.is-drag {
  border-color: var(--red);
  background: color-mix(in srgb, var(--red-deep) 12%, var(--black-3));
}
.upload-zone-title {
  font-family: var(--font-display);
  font-size: 13px;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--white);
}
.upload-zone-sub {
  font-size: 11px;
  margin-top: 4px;
  color: var(--white-dim);
}

.maps-list {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
  gap: 10px;
}
.map-card {
  display: flex;
  flex-direction: column;
  background: var(--black-2);
  border: 1px solid var(--grey-dark);
  border-radius: var(--radius-sm);
  cursor: pointer;
  overflow: hidden;
  transition: border-color 0.15s, transform 0.05s;
}
.map-card:hover { border-color: var(--red); }
.map-card:active { transform: scale(0.99); }
.map-card-thumb {
  aspect-ratio: 16 / 10;
  background-color: var(--black-3);
  background-size: cover;
  background-position: center;
}
.map-card-meta { padding: 8px 10px; }
.map-card-name {
  font-family: var(--font-display);
  font-size: 13px;
  font-weight: 700;
  color: var(--white);
}
.map-card-sub {
  font-size: 11px;
  margin-top: 2px;
}

/* Full-screen viewer overlay — same dd-modal-overlay pattern, but
   the body holds the image at its natural aspect ratio with markers
   absolutely positioned over it. Pinch-zoom works through the
   browser's default behavior on the underlying <img>. */
.maps-viewer {
  position: fixed;
  inset: 0;
  background: var(--overlay-deep);
  z-index: 1090;
  display: none;
  flex-direction: column;
}
.maps-viewer.is-open { display: flex; }
.maps-viewer-head {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 10px 14px;
  border-bottom: 1px solid var(--grey-dark);
  background: var(--black-2);
}
.maps-viewer-title {
  flex: 1;
  font-family: var(--font-display);
  font-size: 14px;
  font-weight: 700;
  color: var(--white);
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.maps-viewer-body {
  flex: 1;
  overflow: auto;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 12px;
  -webkit-overflow-scrolling: touch;
}
.maps-viewer-wrap {
  position: relative;
  display: inline-block;
  max-width: 100%;
}
.maps-viewer-wrap img {
  display: block;
  max-width: 100%;
  max-height: calc(100vh - 140px);
  height: auto;
  user-select: none;
  -webkit-user-drag: none;
}
.maps-viewer-foot {
  padding: 8px 14px;
  background: var(--black-2);
  border-top: 1px solid var(--grey-dark);
  display: flex;
  align-items: center;
  gap: 10px;
}
.maps-viewer-hint {
  flex: 1;
  font-size: 12px;
  color: var(--white-dim);
}

/* Marker pin — RECTANGLE (LM-17). Small label tab so a glance at
   the map reads as "named landmarks", not anonymous dots. Visited
   pins get a green checkmark badge + tinted background. */
.map-pin {
  position: absolute;
  transform: translate(-50%, -100%);
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 3px 8px;
  background: var(--red);
  color: var(--white);
  border: 1px solid var(--red-bright);
  border-radius: var(--radius-sm);
  font-family: var(--font-display);
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.04em;
  cursor: pointer;
  white-space: nowrap;
  max-width: 200px;
  box-shadow: 0 2px 6px var(--shadow-soft);
  z-index: 2;
}
.map-pin:hover { background: var(--red-bright); }
.map-pin .map-pin-label {
  overflow: hidden;
  text-overflow: ellipsis;
  max-width: 160px;
}
.map-pin.is-visited {
  background: #1f6f3b;
  border-color: #34a05a;
}
.map-pin.is-visited:hover { background: #2a8a4a; }

/* Marker placement / detail modals reuse .dd-modal-overlay structure
   but we toggle them with a local is-open class so they don't
   collide with other dd-modal-overlay instances on the page. */
.maps-marker-modal,
.maps-detail-modal {
  position: fixed;
  inset: 0;
  background: var(--shadow-70);
  display: none;
  align-items: center;
  justify-content: center;
  z-index: 1100;
  padding: 16px;
}
.maps-marker-modal.is-open,
.maps-detail-modal.is-open { display: flex; }
.maps-marker-modal .dd-modal-card,
.maps-detail-modal .dd-modal-card {
  max-width: 480px;
  width: 100%;
}
/* ════════════════════════════════════════════════════════════════════
   Pain #47 — wiki polish: tag chips, relationship chips, Cmd+K modal
   ──────────────────────────────────────────────────────────────────
   Tag colour comes from inline `--wiki-h` (a hue 0–360) baked by the
   server hash so the same tag is the same colour on every surface.
   Rectangles only (LM-17) — var(--radius-sm).
   ════════════════════════════════════════════════════════════════════ */
.wiki-chips {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin: 6px 0 0;
}
.wiki-tag {
  display: inline-flex;
  align-items: center;
  font-family: var(--font-display);
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.04em;
  padding: 3px 8px;
  border-radius: var(--radius-sm);
  background: hsl(var(--wiki-h, 0), 55%, 22%);
  color: hsl(var(--wiki-h, 0), 90%, 78%);
  border: 1px solid hsl(var(--wiki-h, 0), 55%, 34%);
  white-space: nowrap;
  line-height: 1.3;
}
.wiki-tag.is-active {
  background: hsl(var(--wiki-h, 0), 65%, 38%);
  color: var(--white);
  border-color: hsl(var(--wiki-h, 0), 70%, 50%);
}
.wiki-tag-filter {
  cursor: pointer;
  user-select: none;
}
.wiki-rel-chip {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  font-family: var(--font-display);
  font-size: 11px;
  font-weight: 600;
  padding: 3px 8px 3px 6px;
  border-radius: var(--radius-sm);
  background: var(--black-3);
  color: var(--grey-light);
  border: 1px solid var(--grey-dark);
  text-decoration: none;
  cursor: pointer;
}
.wiki-rel-chip:hover { background: var(--black-2); color: var(--white); border-color: var(--red); }
.wiki-rel-chip svg { width: 12px; height: 12px; flex: 0 0 auto; }
.wiki-rel-chip .wiki-rel-type {
  font-size: 9px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--grey-text);
  margin-right: 2px;
}
.wiki-rel-del {
  border: none; background: transparent;
  color: var(--grey-text); cursor: pointer;
  padding: 0 0 0 4px; line-height: 1;
  font-size: 13px;
}
.wiki-rel-del:hover { color: var(--red-bright); }

/* Visual chip-graph: chip rows with a connecting bar on the left so
   the user reads them as a hierarchy emanating from the entity. */
.wiki-rel-graph {
  position: relative;
  margin-top: 8px;
  padding: 8px 8px 8px 14px;
  border-left: 2px solid var(--red);
  background: var(--black-4);
  border-radius: var(--radius-sm);
  display: flex;
  flex-direction: column;
  gap: 6px;
}
.wiki-rel-graph .wiki-rel-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px;
}

/* Cmd+K Quick Lookup overlay — built on the .dd-modal-overlay shell
   but with a top-anchored card (search-bar feel) and a long results
   list. */
.dd-modal-overlay.wiki-quick-overlay {
  align-items: flex-start;
  padding-top: max(48px, env(safe-area-inset-top));
}
.wiki-quick-overlay .dd-modal-card {
  width: min(640px, calc(100% - 24px));
  max-height: 80vh;
  display: flex;
  flex-direction: column;
}
.wiki-quick-input-row {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 10px 14px;
  border-bottom: 1px solid var(--black-4);
}
.wiki-quick-input-row svg { width: 18px; height: 18px; color: var(--grey-text); flex: 0 0 auto; }
.wiki-quick-input {
  flex: 1 1 auto;
  background: transparent;
  border: none;
  outline: none;
  color: var(--white);
  font-family: var(--font-display);
  font-size: 16px;
  padding: 4px 0;
}
.wiki-quick-input::placeholder { color: var(--grey-text); }
.wiki-quick-results {
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  flex: 1 1 auto;
  padding: 8px 0;
}
.wiki-quick-section-head {
  font-family: var(--font-display);
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--grey-text);
  padding: 8px 14px 4px;
}
.wiki-quick-row {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 14px;
  cursor: pointer;
  border: none;
  background: transparent;
  width: 100%;
  text-align: left;
  font-family: var(--font-display);
  color: var(--white);
}
.wiki-quick-row:hover, .wiki-quick-row.is-active {
  background: var(--black-3);
}
.wiki-quick-row .wiki-quick-kind {
  font-size: 9px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--grey-text);
  min-width: 56px;
  border-right: 1px solid var(--black-2);
  padding-right: 8px;
}
.wiki-quick-row .wiki-quick-name { font-size: 14px; font-weight: 600; }
.wiki-quick-row .wiki-quick-sub  { font-size: 11px; color: var(--grey-text); margin-top: 1px; }
.wiki-quick-empty {
  padding: 24px 14px;
  text-align: center;
  color: var(--grey-text);
  font-size: 13px;
}
.wiki-quick-hint {
  padding: 6px 14px 10px;
  color: var(--grey-text);
  font-size: 11px;
  border-top: 1px solid var(--black-4);
  display: flex;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 6px;
}
.wiki-kbd {
  font-family: var(--font-mono);
  font-size: 10px;
  background: var(--black-3);
  color: var(--grey-light);
  border: 1px solid var(--grey-dark);
  border-radius: var(--radius-sm);
  padding: 1px 5px;
}


/* Bundle J — Battery Saver (Pain #59).
   Less aggressive than dd-no-anim: dice-roll feedback, button
   active states, and explicit user-triggered animation are
   preserved (those are micro-feedback, not "background"). What
   gets killed: the always-running pulse / fade / shimmer chrome
   that drains battery on a sheet sitting open on the table for a
   4-hour session. The pre-paint hook in base.html adds the class
   so the suppression lands on first paint, no flash.
   Polling cadence stretching is handled in static/js/visibility.js
   via DD.poll. */
html.dd-battery-saver #dm_xcard_alert_card,
html.dd-battery-saver .pulse,
html.dd-battery-saver .pulse-dot,
html.dd-battery-saver .turn-flag-pulse,
html.dd-battery-saver .dm-pc-card.is-current-turn::after,
html.dd-battery-saver .enc-row.current::after,
html.dd-battery-saver .shimmer,
html.dd-battery-saver .breathe,
html.dd-battery-saver .glow-anim,
html.dd-battery-saver .mt-group-alert::before {
  animation: none !important;
}
/* Strip the long-running background gradient / glow animations on
   chrome that's always visible. Each rule explicit so we don't
   accidentally kill button-press feedback or dice-stage anticipation. */
html.dd-battery-saver .toast,
html.dd-battery-saver .dd-toast,
html.dd-battery-saver .roll-log-row,
html.dd-battery-saver .chip,
html.dd-battery-saver .nav-item,
html.dd-battery-saver .section,
html.dd-battery-saver .card {
  transition-duration: 0.08s !important;
}


/* ============================================================
   D&D EXPERIENCE TIER  (data-xp on <html>)
   ------------------------------------------------------------
   Three tiers — "simple" (max guidance), "standard" (canonical
   default), "advanced" (minimal scaffolding). The setting lives
   on User.experience_level and is set from the user_settings
   page; base.html stamps it as data-xp on <html>. DM dashboard
   pages override to "standard" so DMs are unaffected.

   Rules below progressively HIDE helper UI as the tier rises.
   "simple" shows everything plus extra guidance; "standard"
   shows everything except the extra guidance ("simple"-only
   bits); "advanced" hides walkthrough CTAs, explanatory copy,
   and tooltips.

   Element opt-in classes that templates use:
     .xp-only-simple       — visible at simple ONLY
     .xp-hide-advanced     — hidden at advanced (visible simple+standard)
     .xp-only-advanced     — visible at advanced ONLY (rare)

   Universal default (no class) — visible at every tier.
   ============================================================ */

/* xp-only-simple: only show this element at the most-helpful tier. */
html:not([data-xp="simple"]) .xp-only-simple { display: none !important; }

/* xp-hide-advanced: hide walkthrough CTAs / explanatory copy that
   experts don't need. Visible at simple + standard. */
html[data-xp="advanced"] .xp-hide-advanced { display: none !important; }

/* xp-only-advanced: rare — for compact-mode replacements. */
html:not([data-xp="advanced"]) .xp-only-advanced { display: none !important; }

/* Picker styles for the user_settings.html xp picker. Vertical list
   of three selectable options with name + sub-line. Per the
   ui_patterns.md ruleset: rectangles, var(--radius-sm), no pills. */
.xp-picker {
  display: flex;
  flex-direction: column;
  gap: 8px;
}
.xp-option {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  padding: 10px 12px;
  border: 1px solid var(--grey-dark);
  border-radius: var(--radius-sm);
  background: var(--black-3);
  cursor: pointer;
  transition: border-color var(--transition), background var(--transition);
}
.xp-option:hover { border-color: var(--grey-mid); }
.xp-option input[type="radio"] {
  margin-top: 3px;
  accent-color: var(--red-bright);
  flex: 0 0 auto;
}
.xp-option-content {
  display: flex;
  flex-direction: column;
  gap: 2px;
  flex: 1 1 auto;
}
.xp-option-content strong {
  font-family: var(--font-display);
  font-size: 13px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--white);
}
.xp-option.selected {
  border-color: var(--red-bright);
  background: var(--red-deep);
}
.xp-option.selected .xp-option-content strong { color: var(--white); }


/* =============================================================
   Distribute Loot modal — per-player picker rows. Each row stacks
   a [checkbox] [name] and a 5-column grid of mini coin inputs
   (PP/GP/EP/SP/CP). The master "Select all" row sits at the top
   with a slightly darker background. Disabled inputs (rows the DM
   has unchecked) dim so the DM can scan who's not receiving.
   Mobile flips the row to column layout so name + coin grid don't
   squeeze on a phone.
   ============================================================= */
.dm-loot-row {
  display: flex; align-items: center; gap: 10px;
  padding: 8px 10px;
  background: var(--black-2);
  border: 1px solid var(--grey-dark);
  border-radius: var(--radius-sm);
  cursor: pointer;
}
.dm-loot-row.dm-loot-master { background: var(--black-3); }
.dm-loot-row > input[type="checkbox"] { flex: 0 0 auto; }
.dm-loot-name {
  flex: 0 0 auto;
  min-width: 100px;
  font-weight: 600;
  color: var(--white);
}
.dm-loot-coins {
  flex: 1 1 auto;
  display: grid;
  grid-template-columns: repeat(5, 1fr);
  gap: 6px;
  min-width: 0;
}
.dm-loot-coin-input {
  display: flex; flex-direction: column; gap: 2px;
  min-width: 0;
}
.dm-loot-coin-input .input { width: 100%; padding: 4px 6px; font-size: 13px; }
.dm-loot-coin-input .small-caps { font-size: 10px; letter-spacing: 0.08em; }
.dm-loot-row input[type="number"]:disabled {
  opacity: 0.45;
  cursor: not-allowed;
}
html.dd-mobile .dm-loot-row {
  flex-direction: column;
  align-items: stretch;
  gap: 8px;
}
html.dd-mobile .dm-loot-name {
  min-width: 0;
}


/* =============================================================
   Stance glow — viewport-edge halo when a stance ability is active

   Painted by static/js/stance_glow.js, which sets
   `data-dd-stance-active="1"` on the html element while any
   `*_active` flag inside __DD_CHAR__.combat_state.class_features
   is true (Rage, Hunter's Mark, Echo, Wild Shape — generic by
   suffix so new stance flags light up automatically).

   The element is a fixed full-viewport overlay with an inset
   box-shadow in the theme accent (--red, which re-tints per
   theme — see base.css theme blocks). pointer-events: none so
   it never blocks input. z-index sits ABOVE page content and
   the splash but BELOW modal overlays (.dd-modal-overlay z-index
   is well above 100). Decorative — aria-hidden.

   prefers-reduced-motion drops the pulse and keeps a static glow
   so users sensitive to animation still get the cue without the
   breathing effect.
   ============================================================= */
.dd-stance-glow {
  position: fixed;
  inset: 0;
  pointer-events: none;
  z-index: 90;
  opacity: 0;
  transition: opacity 240ms ease-out;
  box-shadow:
    inset 0 0 60px 6px color-mix(in srgb, var(--red) 70%, transparent),
    inset 0 0 14px 0   color-mix(in srgb, var(--red) 85%, transparent);
}
html[data-dd-stance-active="1"] .dd-stance-glow {
  opacity: 0.65;
  animation: dd-stance-glow-pulse 3.2s ease-in-out infinite;
}
@keyframes dd-stance-glow-pulse {
  0%, 100% {
    box-shadow:
      inset 0 0 56px 4px  color-mix(in srgb, var(--red) 60%, transparent),
      inset 0 0 14px 0    color-mix(in srgb, var(--red) 80%, transparent);
  }
  50% {
    box-shadow:
      inset 0 0 96px 12px color-mix(in srgb, var(--red) 85%, transparent),
      inset 0 0 24px 2px  var(--red);
  }
}
@media (prefers-reduced-motion: reduce) {
  html[data-dd-stance-active="1"] .dd-stance-glow {
    animation: none;
  }
}

/* ───────────────────────────────────────────────────────────────────
   Left-edge accent stripes removed — 2026-06-15 (user request).
   The COLORED left-stripe motif is flattened app-wide. Pre-existing
   neutral-grey STRUCTURAL borders, .enc-status (encumbrance), and the
   currency top strips (.currency-cell border-top) are intentionally
   preserved. Functional color cues that rode on the left stripe (NPC
   disposition, my-turn zones, initiative turn, death-saves, exhaustion,
   over-prepared) are flattened per the user's explicit choice. To
   restore the motif, delete this block. The original colored rules are
   left in place above; this block overrides them by source order.
   ─────────────────────────────────────────────────────────────────── */

/* (a) Stripe-only boxes (no other border) → drop the left edge entirely. */
.ua-danger-card, .db-render-dm,
.mt-zone-now, .mt-zone-choose, .mt-zone-end,
.mt-cond-row, .mt-buff-row,
.auth-hint, .install-hint, .wiki-rel-graph,
.exh-status { border-left: none; }

/* (b) Boxes WITH a full border → restore the left to match the box
       outline so no side is left open; only the thick accent goes. */
.cf-module, .feat-effects-preview, .rmap-header,
.prepared-counter { border-left: 1px solid var(--grey-dark); }
.dd-install-promo { border-left: 1px solid var(--red-border); }
.cf-module.active, .prepared-counter.over { border-left-color: var(--grey-dark); }

/* (c) Structural-grey base + colored STATE override → flatten the
       override back to the surviving grey base (grey stripe stays). */
.npc-card.friendly, .npc-card.hostile, .npc-card.complex { border-left-color: var(--grey-mid); }
.init-row.current, .init-row.is-self { border-left-color: var(--grey-dark); }
.ds-card.ds-dying, .ds-card.ds-stable, .ds-card.ds-dead { border-left-color: var(--grey-dark); }
