/* ============================================================
   Data Dungeon — design tokens + global rules
   ============================================================ */
/* ============================================================
   CROSS-DOCUMENT VIEW TRANSITIONS  (see CLAUDE.md LM-17)
   ------------------------------------------------------------
   Every tab click is a full HTTP GET. Without this, the browser
   cuts instantly from outgoing to incoming — persistent chrome
   (logo, header icons, nav bars, chip strip) flickers on every
   click. With `@view-transition { navigation: auto; }`, the
   browser captures an outgoing snapshot, loads the new page,
   then animates the swap. Elements tagged with
   `view-transition-name` are treated as shared across the two
   pages and stay put (no flicker). Everything else cross-fades
   + drifts 6px over 250ms so switches read as a gentle turn.

   Support: Chrome 126+, Safari 18.2+ (iOS 18.2+). Older
   browsers silently fall back to instant navigation — no
   regression. Do not re-add or rename these without checking
   LM-17 first.
   ============================================================ */
@view-transition { navigation: auto; }

/* Persistent-chrome names. These MUST be unique per page —
   duplicate names abort the transition. Only add names to
   elements that appear on EVERY navigable page (or that always
   appear on both pages of a given transition pair). Don't name
   ephemeral UI: modals, toasts, popups, concentration banners,
   flash messages. See LM-17. */
.app-header   { view-transition-name: dd-app-header;   }
.app-logo     { view-transition-name: dd-app-logo;     }
.header-right { view-transition-name: dd-header-right; }
.app-nav      { view-transition-name: dd-app-nav;      }
.top-nav      { view-transition-name: dd-top-nav;      }
.bottom-nav   { view-transition-name: dd-bottom-nav;   }
.chip-strip   { view-transition-name: dd-chip-strip;   }

/* Named-element groups: no animation. If geometry differs
   between pages, the browser would morph — we want them to
   stay still. */
::view-transition-group(dd-app-header),
::view-transition-group(dd-app-logo),
::view-transition-group(dd-header-right),
::view-transition-group(dd-app-nav),
::view-transition-group(dd-top-nav),
::view-transition-group(dd-bottom-nav),
::view-transition-group(dd-chip-strip) {
  animation-duration: 0s;
}

/* Root (the unnamed page-body remainder): soft cross-fade +
   subtle vertical drift. ~0.25s, standard material easing. */
::view-transition-old(root),
::view-transition-new(root) {
  animation-duration: 0.25s;
  animation-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
  animation-fill-mode: both;
}
::view-transition-old(root) { animation-name: dd-vt-fade-slide-out; }
::view-transition-new(root) { animation-name: dd-vt-fade-slide-in;  }

@keyframes dd-vt-fade-slide-out {
  from { opacity: 1; transform: translateY(0); }
  to   { opacity: 0; transform: translateY(-6px); }
}
@keyframes dd-vt-fade-slide-in {
  from { opacity: 0; transform: translateY(6px); }
  to   { opacity: 1; transform: translateY(0); }
}

/* Accessibility: honor prefers-reduced-motion by collapsing
   the animation to an imperceptible tick. Chrome stays put
   regardless. */
@media (prefers-reduced-motion: reduce) {
  ::view-transition-old(root),
  ::view-transition-new(root) {
    animation-duration: 0.01s;
  }
}

/* ============================================================
   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: rgba(0,0,0,0.78);
  /* 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; }

:root {
  --black:       #0a0a0a;
  --black-2:     #111111;
  --black-3:     #181818;
  --black-4:     #222222;
  --grey-dark:   #2e2e2e;
  --grey-mid:    #444444;
  --grey-light:  #888888;
  --grey-text:   #aaaaaa;
  --white-dim:   #cccccc;
  --white:       #f0f0f0;

  --red:         #cc2222;
  --red-bright:  #e63333;
  --red-deep:    #8b0000;
  --red-glow:    rgba(204,34,34,0.18);
  --red-border:  rgba(204,34,34,0.35);

  /* ── 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);
}
/* 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; }

/* 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;
}
.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: #7aa6dc; 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-2 { margin-top: 2px; } .mt-6 { margin-top: 6px; } .mt-8 { margin-top: 8px; }
.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;
}

/* 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: 3px solid var(--red);
}

.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);
}

/* ---- 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);
  padding: 4px 0;
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.55);
  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-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); }
.dd-stepper-btn:active { background: var(--red-deep); color: var(--red-bright); }
.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;
}
.btn-primary { background: var(--red); color: var(--white); }
.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; }

.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: 3px solid var(--red);
  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 rgba(0,0,0,0.4);
}
.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: #8be09c;
}
.hp-btn-heal:hover { background: rgba(60,180,90,0.22); }
.hp-btn-heal:active,
.hp-btn-heal.pressed {
  background: #3c9453;
  border-color: #3c9453;
  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; }
.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 rgba(0,0,0,0.6);
  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); }
.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: 3px solid var(--red);
  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;
}

.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(--red-bright); }
.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); }

.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; 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 ---- */
.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; }
.npc-notes { font-size: 13px; color: var(--grey-text); margin-top: 6px; line-height: 1.5; }

/* ---- 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; }

/* ---- 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 {
  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));
  /* Force a compositor layer so iOS keeps the nav pinned to the
     viewport during momentum-scroll instead of letting it drift up
     with the page. Pairs with the -webkit-fill-available fix on
     html/body above. */
  transform: translateZ(0);
  will-change: transform;
  -webkit-backface-visibility: hidden;
          backface-visibility: hidden;
}
.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;
  border-radius: 999px;
  background: var(--red);
  color: #fff;
  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 ---- */
/* 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: var(--white);
  border: 1px solid var(--red-border);
  border-radius: 50%;
  cursor: pointer;
  box-shadow: 0 4px 14px rgba(0, 0, 0, 0.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 rgba(0, 0, 0, 0.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));
}

.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 rgba(0,0,0,0.5);
}
.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: rgba(0,0,0,0.92);
  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-header {
  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 {
  width: 44px; height: 44px;
  background: var(--black-4);
  border: 1px solid var(--grey-dark);
  border-radius: var(--radius-sm);
  color: var(--grey-text);
  font-size: 20px;
  cursor: pointer;
}
.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 { 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 walkthrough ----
   Seven expandable phases (details/summary) that operate as a
   checklist wizard: picking an action inside a phase (or tapping the
   inline Next Step button) closes the phase, marks it done, and
   auto-opens the next. Only the currently-active phase carries the
   accent highlight; the highlight migrates as the user progresses. */
.mt-step {
  background: var(--black-3);
  border: 1px solid var(--grey-dark);
  border-radius: var(--radius);
  margin-bottom: 10px;
  overflow: hidden;
  transition: border-color var(--transition), box-shadow var(--transition), opacity var(--transition);
}
/* Only the ACTIVE step gets the red accent ring — moves as the user
   advances, so no phase carries a perma-highlight. */
.mt-step.mt-step-active {
  border-color: var(--red);
  box-shadow: 0 0 0 1px var(--red-border);
}
/* Completed steps fade back and pick up a green check chip. */
.mt-step.mt-step-done {
  opacity: 0.7;
  border-color: rgba(16, 185, 129, 0.35);
}
.mt-step.mt-step-done .mt-step-num {
  background: rgba(16, 185, 129, 0.18);
  color: #86efac;
}
.mt-step.mt-step-done .mt-step-num-n { display: none; }
.mt-step.mt-step-done .mt-step-num-check { display: block; }
.mt-step-summary {
  display: grid;
  grid-template-columns: 28px 1fr auto 20px;
  align-items: center;
  gap: 10px;
  padding: 12px 14px;
  cursor: pointer;
  list-style: none;
  min-height: 0;
}
.mt-step-summary::-webkit-details-marker { display: none; }
.mt-step-summary::marker { content: ''; }
.mt-step-num {
  position: relative;
  width: 26px; height: 26px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: 50%;
  background: var(--red-deep);
  color: var(--red-bright);
  font-family: var(--font-mono);
  font-weight: 700;
  font-size: 13px;
  transition: background var(--transition), color var(--transition);
}
.mt-step-num-n { line-height: 1; }
.mt-step-num-check { display: none; }
.mt-step-title {
  font-family: var(--font-display);
  font-size: 14px;
  font-weight: 700;
  letter-spacing: 0.06em;
  color: var(--white);
}
.mt-step-sub {
  font-family: var(--font-display);
  font-size: 11px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--grey-text);
}
.mt-step-chev {
  color: var(--grey-text);
  transition: transform 0.18s ease;
}
.mt-step[open] > .mt-step-summary .mt-step-chev {
  transform: rotate(180deg);
}
.mt-step-body {
  padding: 4px 14px 14px;
  border-top: 1px solid var(--grey-dark);
  display: flex;
  flex-direction: column;
  gap: 8px;
}
.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"; carries
   an optional inline CTA that skips the walkthrough ahead to Phase 7
   where the save lives. */
.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: #ffb85a; }
.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-banner-cta {
  align-self: flex-start;
  padding: 8px 16px;
  min-height: 36px;
  min-width: 0;
}
.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 Phase 3 target panel).
   Cross-references DM-revealed enemy info with the player's own
   character data — see LM-12. */
.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;
  border-radius: 999px;
  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; }
/* Inline per-step advance button. Sits at the end of each phase and
   advances to the next when tapped; also auto-fires when the user
   picks any action inside the phase. Compact, centered — no
   min-width stretch, text is centered inside the button. */
.mt-step-next {
  align-self: center;
  margin-top: 8px;
  padding: 8px 20px;
  min-height: 36px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  text-align: center;
  width: auto;
  min-width: 0;
}

/* "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;
  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; }
.dm-pc-btn-apply       { background: var(--red-deep); color: var(--red-bright); 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;
}
.condition-badge.active { background: var(--red-deep); color: var(--red-bright); 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,
    #3c9453  0%,      #3c9453 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 rgba(0,0,0,0.4) 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: rgba(0,0,0,0.5);
  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: #3c9453; }
.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: #8be09c; }
.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;
  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 {
  font-family: var(--font-display);
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  padding: 6px 10px;
  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); }

.wizard-actions {
  display: flex;
  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); }

/* 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);
  color: var(--red-bright);
}

/* ---- 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 rgba(0,0,0,0.6);
  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);
  border: 1px solid var(--red-border);
  border-left: 3px solid var(--red);
  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 rgba(0,0,0,0.65);
  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; }
.toast.success { border-left-color: #2a7a2a; }
.toast.error { border-left-color: var(--red-bright); }
.toast.info { border-left-color: var(--red); }
.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 { background: #4cc74c; }
.toast.error .tdot { background: var(--red-bright); box-shadow: 0 0 8px var(--red); }

/* ---- 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);
  color: var(--red-bright);
}
.chip.adv-on { background: rgba(70,140,220,0.20); border-color: rgba(70,140,220,0.5); color: #9cc8f4; }
.chip.dis-on { background: rgba(220,90,90,0.20); border-color: rgba(220,90,90,0.5); color: #f59c9c; }
.chip.warn   { background: rgba(139,105,20,0.22); border-color: rgba(200,160,60,0.55); color: #e4ce7a; }
.chip.good   { background: rgba(42,122,42,0.22); border-color: rgba(60,200,90,0.55); color: #9ee39c; }

/* ---- 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 rgba(0, 0, 0, 0.4) inset;
}
.party-hp-tick {
  position: absolute;
  top: 0; bottom: 0;
  width: 1px;
  background: rgba(0, 0, 0, 0.5);
  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;
  border-radius: 999px;
  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: #4cc74c; }
.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;
}

/* ---- 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: #4cc74c; border-color: #4cc74c; box-shadow: 0 0 4px #4cc74c; }
.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: #4cc74c; }
.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;
  border-radius: 999px;
  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: #8be09c;          border-color: #3c9453;            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: #8be09c; }
.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: #d9a849; border-color: #d9a849; box-shadow: 0 0 6px rgba(217,168,73,0.4); }
.exh-seg.on.exh-seg-2 { background: #d9a849; border-color: #d9a849; }
.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: #3c9453; }
.exh-level-0 .exh-status-label { color: #8be09c; }
.exh-level-1        { border-left-color: #d9a849; }
.exh-level-1 .exh-status-label,
.exh-level-2 .exh-status-label { color: #e8c84a; }
.exh-level-2        { border-left-color: #d9a849; }
.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: #d9a849; }
.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;
  border-radius: 999px;
  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;
}
/* No visible box on 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. */
.header-icon-btn:hover {
  color: var(--white);
  background: var(--black-3);
}
.header-icon-btn:focus,
.header-icon-btn:focus-visible {
  outline: none;
  border-color: transparent;
}
.header-icon-btn:active { color: var(--red-bright); border-color: transparent; }
.header-icon-btn svg { display: block; }

/* ---- Auth (login / register) ---- */
.auth-card {
  background: var(--black-2);
  border: 1px solid var(--grey-dark);
  border-top: 3px solid var(--red);
  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 rgba(0,0,0,0.65);
  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); }

/* ---- 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,
.chip-gear:focus-visible { color: var(--white); outline: none; }
.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: rgba(0,0,0,0.78);
  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
   ------------------------------------------------------------
   The app's dark chrome is constant. Each [data-theme="slug"]
   just overrides the five accent (--red-*) variables. Every
   stylesheet references these via var(--red), var(--red-bright)
   etc., so swapping them re-skins the app with no other work.
   Keep in sync with services/themes.py.
   ============================================================ */

/* "crimson" — default, matches :root; listed for completeness. */
[data-theme="crimson"] {
  --red:        #cc2222;
  --red-bright: #e63333;
  --red-deep:   #8b0000;
  --red-glow:   rgba(204,34,34,0.18);
  --red-border: rgba(204,34,34,0.35);
}
[data-theme="emerald"] {
  --red:        #10b981;
  --red-bright: #34d399;
  --red-deep:   #065f46;
  --red-glow:   rgba(16,185,129,0.16);
  --red-border: rgba(16,185,129,0.35);
}
[data-theme="sapphire"] {
  --red:        #2563eb;
  --red-bright: #60a5fa;
  --red-deep:   #1e3a8a;
  --red-glow:   rgba(37,99,235,0.18);
  --red-border: rgba(37,99,235,0.4);
}
[data-theme="amber"] {
  --red:        #d97706;
  --red-bright: #fbbf24;
  --red-deep:   #78350f;
  --red-glow:   rgba(217,119,6,0.18);
  --red-border: rgba(217,119,6,0.4);
}
[data-theme="violet"] {
  --red:        #7c3aed;
  --red-bright: #a78bfa;
  --red-deep:   #4c1d95;
  --red-glow:   rgba(124,58,237,0.18);
  --red-border: rgba(124,58,237,0.4);
}
[data-theme="rose"] {
  --red:        #e11d48;
  --red-bright: #fb7185;
  --red-deep:   #881337;
  --red-glow:   rgba(225,29,72,0.18);
  --red-border: rgba(225,29,72,0.4);
}
[data-theme="slate"] {
  --red:        #64748b;
  --red-bright: #94a3b8;
  --red-deep:   #334155;
  --red-glow:   rgba(100,116,139,0.18);
  --red-border: rgba(100,116,139,0.45);
}

/* ---- 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 rgba(0, 0, 0, 0.55));
  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: rgba(0, 0, 0, 0.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 rgba(0, 0, 0, 0.4),
    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;
}
.dd-tutorial-bubble {
  position: fixed;
  width: 300px;
  max-width: calc(100vw - 32px);
  background: var(--black-2);
  border: 1px solid var(--grey-dark);
  border-top: 3px solid var(--red);
  border-radius: var(--radius);
  padding: 14px 14px 12px;
  box-shadow: 0 14px 40px rgba(0, 0, 0, 0.65);
  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 rgba(0, 0, 0, 0.55));
  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,
.dd-tut-pick-row:focus-visible { background: var(--red-glow); outline: none; }
.dd-tut-pick-avatar {
  width: 44px;
  height: auto;
  flex-shrink: 0;
  image-rendering: pixelated;
  filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.5));
}
.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 rgba(0, 0, 0, 0.6);
  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 rgba(0, 0, 0, 0.6);
  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 ── */
.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);
}
.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;
}
