/* web/styles.css — real client palette. Iron Sanctum re-skin (design canvas
   Durnvale-print-9g3j8m.dc.html, sheet 5a + component language 3a). Dark-fantasy warm
   iron/bronze; mobile-first (390) with a desktop frame (>=1024). Skin, not a redesign —
   the information architecture below is unchanged; only chrome, type and the token layer.

   THE TOKEN LAYER (5a). Before this pass every color was a hardcoded hex; these variables
   are the single source of truth for the re-skin. Rarity hues are RESERVED: the four
   strict ones (--r-uncommon/-rare/-epic/-mythic) may appear ONLY inside rarity-scoped
   classes (the r-dash and s-dash rarity classes, and rarity keyframes) — enforced by
   tests/test_styles_guard.py. --ember
   and Legendary deliberately SHARE #E0B25A (5a: "context disambiguates"), so #E0B25A is
   the one rarity-adjacent value allowed anywhere (it is also the sanctioned warm accent). */
:root {
  color-scheme: dark;
  /* base & chrome — COLD iron (Post-Pass-8 P1). The base is blue-black / desaturated slate
     so the shadow is a COLDER temperature than the warm ember accent (the old warm-brown
     #262C36 iron made the whole UI read sepia). The splash art is cool teal; the UI it opens
     into is cold, with warm highlights only. R <= B on every chrome value below. */
  --void:       #08090C;   /* app bg / deepest ground */
  --panel:      #0E1014;   /* panel fill */
  --panel-raise:#171B22;   /* a lifted card / selected row (derived step) */
  --inset:      #090A0D;   /* recessed well / track / disabled (derived step) */
  --iron:       #262C36;   /* 1px iron border — cold slate, NOT brown */
  --iron-soft:  #1A1E25;   /* dimmer border / hairline (derived step) */
  --bronze:     #8A6A2E;   /* the ONE warm chrome: CTA edge / bracket / rule (kept sparingly) */
  --bronze-lo:  #5E4A22;   /* the dark stop of the forged-bronze CTA gradient (derived) */
  --ember:      #E7A83E;   /* the warm accent (GS / CTA edge-glow / energy fill / active nav) —
                              its OWN bronze-amber, DISTINCT from Legendary (no rarity alias). */
  --parchment:  #E6D5A8;   /* headings / world-voice names (the warm text highlight) */
  --body:       #C3B48E;   /* body text */
  --muted:      #6A6E77;   /* dim / tracked micro-labels — cool grey, not sepia brown */
  --danger:     #C4553E;   /* warning / error / defeat-adjacent — a WARM rust, NOT mythic */
  /* rarity — reserved, never decorative. NONE may be aliased to a non-rarity token
     (enforced by tests/test_styles_guard.py). --ember is deliberately its OWN hue now. */
  --r-common:   #9C927A;
  --r-uncommon: #5AC67A;
  --r-rare:     #4A9EFF;
  --r-epic:     #A56BFF;
  --r-legendary:#E0B25A;   /* Legendary keeps the canonical gold; ember is now separate */
  --r-mythic:   #E5484D;
  /* glow law — two sources only: ember/bronze and item rarity. Nothing else. */
  --ember-glow: 0 0 18px -8px rgba(201,154,76,.5);
  /* type law (5a): Spectral = world-voice (self-hosted below); Plex Sans = interface. */
  --serif: 'Spectral', Georgia, 'Times New Roman', serif;
  --sans:  'IBM Plex Sans', 'Segoe UI', system-ui, sans-serif;
}
/* Self-hosted type (5a: "self-host both"). Latin woff2, served from web/fonts/ (the
   whole web/ dir is mounted at / — see main.py). font-display:swap so the Georgia/
   system-ui fallbacks in --serif/--sans paint instantly and never block a hunt. */
@font-face { font-family:'Spectral'; font-style:normal; font-weight:400;
  src:url('fonts/spectral-400.woff2') format('woff2'); font-display:swap; }
@font-face { font-family:'Spectral'; font-style:normal; font-weight:600;
  src:url('fonts/spectral-600.woff2') format('woff2'); font-display:swap; }
@font-face { font-family:'Spectral'; font-style:italic; font-weight:400;
  src:url('fonts/spectral-400i.woff2') format('woff2'); font-display:swap; }
@font-face { font-family:'IBM Plex Sans'; font-style:normal; font-weight:400 600;
  src:url('fonts/plexsans.woff2') format('woff2'); font-display:swap; }

body { margin:0; background:var(--void); color:var(--body); font-family:var(--sans);
       display:flex; flex-direction:column; min-height:100vh;
       min-height:100dvh; }  /* iOS: 100vh over-counts the URL-bar area; dvh is the visible box */

/* Header strip: energy / gold / dust. Stays fixed in the header across all tabs. */
#strip { display:flex; align-items:center; gap:10px; justify-content:space-between;
         /* iOS standalone (black-translucent status bar / notch): keep the wallet clear of
            the status bar + side notch. env() is 0 everywhere except a notched safe-area. */
         padding:calc(10px + env(safe-area-inset-top,0px)) calc(14px + env(safe-area-inset-right,0px))
                 10px calc(14px + env(safe-area-inset-left,0px));
         background:#0E1014; border-bottom:1px solid #262C36; font-size:14px; }
.ebar { flex:1; height:7px; background:#191D24; border-radius:4px; overflow:hidden; margin:0 2px; }
.ebar>div { height:100%; background:linear-gradient(90deg,#8A6A2E,#E7A83E); }
#strip .gold { color:#E6D5A8; }
#strip .dust { color:#C3B48E; }
/* FIX #16 (P2, Trader legibility): "the wallet tick-animates on a completed
   transaction" — app.js's pulseWallet() toggles .tick after a sell/buy lands. */
@keyframes goldtick { 0%{transform:scale(1)} 35%{transform:scale(1.22);text-shadow:0 0 10px #E6D5A8cc} 100%{transform:scale(1)} }
#strip .gold.tick { display:inline-block; animation:goldtick .5s ease; }

main { flex:1; padding:16px; max-width:430px; margin:0 auto; width:100%; box-sizing:border-box; }

/* ---- FIX #18 (P2, owner playtest): desktop/wide-viewport frame ------------------
   "Bloomberg-terminal-meets-anime" -- centers the WHOLE app (header strip included:
   #strip is a direct `body` child alongside `main`/`#tabs`/`#toast`, so capping
   BODY's own width pulls the currency strip inside the same bordered column too,
   not just `main`, which already had its own independent 430px cap above) in a
   ~480px bordered column with a dark radial vignette glowing behind it.

   Gated behind a min-width media query: every rule above this block (body's own
   base flex/background/min-height, #strip, #tabs, main's own 430px cap, the entire
   rest of this file) is completely UNCHANGED below the breakpoint -- the 390px-first
   phone layout this file opens with is untouched, verified live at 390px width.

   No overflow:hidden on body: #reveal/#toast/#itemModal/#screenflash (T20-T23, FIX
   #13) are position:fixed children of body, and an overflow:hidden ancestor would
   visually clip those full-viewport overlays -- the frame is a border + shadow only,
   never a clipping box. */
@media (min-width: 768px) {
  /* Spec-number normalization (Step-2 item 6): the frame band starts at the committed
     spec's 768, not the pre-existing 700 — 700–767 viewers now get the mobile layout the
     spec prescribes for them. */
  /* Margin wash layer A (6a): a cool radial vignette filling the gutters — the margins are
     atmosphere, not void. Layer B (the faint active-zone banner) is #bg-zone, set by JS. */
  html { background: radial-gradient(130% 100% at 50% -10%, #10161F 0%, #08090C 62%); }
  #bg-zone { position:fixed; inset:0; z-index:-1; pointer-events:none; opacity:.06;
    background-size:cover; background-position:center top; filter:blur(10px);
    -webkit-mask-image:radial-gradient(120% 90% at 50% 0%, #000 30%, transparent 78%);
            mask-image:radial-gradient(120% 90% at 50% 0%, #000 30%, transparent 78%); }
  /* min-height overridden (not just added to) here: body's base rule above is
     `min-height:100vh` — left as-is, this 24px top+bottom margin would push the
     panel's total occupied height past 100vh, forcing a scroll just to reach the
     bottom tab nav (confirmed live before this line was added). calc(100vh - 48px)
     + the 2×24px margin nets back out to exactly 100vh — keep the pair in step.
     Item 6: margin snapped to the 4/8/12/16/24/32/48 scale (was 28), the leftover
     1px ring dropped, the shadow normalized to the spec lift shadow. */
  body { max-width:480px; margin:24px auto; min-height:calc(100vh - 48px);
         border:1px solid #262C36; box-shadow:0 40px 120px -40px #000; }
  /* Gold-value truncation guard: at the frame's ~480px width (far narrower than an
     unbounded desktop window, where #strip previously had unlimited room to grow
     into and this pressure never existed), a large toLocaleString() gold number
     ("🪙 1,234,567") competing with the energy bar's own `flex:1` growth could
     otherwise wrap/squeeze inside the header strip. Only the energy bar (purely
     decorative) is left free to shrink; the numeric/icon text spans hold their
     natural single-line width. */
  #strip .ico, #strip .eval, #strip .gold, #strip .dust { flex:0 0 auto; white-space:nowrap; }
}

/* #tabs' own base (mobile bottom-bar) rule — relocated here (content byte-for-byte
   unchanged, only its position in the file moved) by FIX #7 (P2, cert Day-1 batch T2
   desktop polish). Root cause of "nav labels float in huge gaps" at >=1024px: this
   file's own established convention (see the >=1024px `body` rule's comment directly
   below) is "equal-specificity conflicts resolve by SOURCE ORDER, later wins" — but
   this rule used to sit AFTER the >=1024px nav-rail override block below (same
   "#tabs"/"#tabs button"/"#tabs .on" selectors, equal specificity), so THIS rule's
   flex:1/padding:12px/font-size:13px/border-top was silently winning the cascade at
   EVERY width, including >=1024px: each nav-rail button stretched to fill an equal
   share of the rail's full (grid-row:1fr, very tall) height instead of packing at the
   top. Moving it to before the >=1024px block restores the intended order, so the
   desktop override's flex:0 0 auto/padding:13px 22px/font-size:14px/border-top:0
   correctly wins there, packing the nav labels into a tight list at the rail's top.
   Nothing here is gated by any media query, so mobile (<1024px) renders byte-for-byte
   what it did before — this is a pure reorder, not a content change. */
#tabs { display:flex; border-top:1px solid #262C36; background:#0E1014;
        padding-bottom:env(safe-area-inset-bottom,0px); }  /* iOS: clear the home indicator */
#tabs button { flex:1; padding:12px; background:none; border:0; color:#6A6E77; font-size:13px; }
#tabs .on { color:#E7A83E; font-weight:700; }

/* ---- T2.2 (desktop layout >=1024px): nav rail + content column + persistent right
   panel --------------------------------------------------------------------------
   SAME DOM as every other breakpoint — no new elements from app.js's own render()
   path. The only new markup anywhere is index.html's static `<aside id="rightpanel">`
   (a sibling of #strip/#screen/#tabs/#toast, same "static mount, app.js owns the
   innerHTML" convention those already use); its CONTENT is written by app.js's
   renderRightPanel(), called every render() alongside renderStrip()/renderChips(),
   straight off S.state — no game logic, pure display (see that function's own
   comment). #tabs is NOT a new nav: it is the SAME three data-tab buttons wireTabs()
   already drives, restyled below into a vertical left rail — the tab-switch JS is
   completely untouched. #screen (main) is the SAME per-tab render() target, just
   widened+recentered into the grid's content column.

   `#rightpanel { display:none; }` right below is unconditional (not gated behind a
   media query) because it is a brand-new element with no prior visibility rule of
   its own — without this, an <aside> with no CSS at all would render as a visible
   block box (zero height in practice since renderRightPanel() only ever populates
   it, but still an unstyled seam) at every width below 1024px. This does NOT alter
   any EXISTING rule (nothing above this line is touched), and the element has zero
   visual footprint either way below 1024px, so mobile (<700px) and the 700-1023px
   desktop-frame band stay pixel-for-pixel what they were before this task —
   confirmed by reading (not editing) every rule above this comment.

   Gated behind min-width:1024px so the 390px mobile layout AND the 700-1023px
   "desktop frame" band (the @media(min-width:768px) block directly above) are
   untouched below this breakpoint. At >=1024px this block's own `body` rule (same
   selector, later in source order, equal specificity) wins the
   max-width/margin/min-height/border/box-shadow/display fight against the 700px
   block's `body` rule above, SUPERSEDING it outright rather than layering on top of
   it — exactly the "let the >=1024px grid supersede it" requirement. */
#rightpanel { display:none; }
#bg-zone { display:none; }  /* the desktop margin-wash zone backdrop — shown only >=1024 */
.loreband { display:none; }  /* desktop vertical-rhythm band (6a §6) — shown only >=1024, so 390px is untouched */

/* Right-panel MODULE internals (6a §3-⌄ / 6c) — BASE-scoped, not desktop-scoped, because
   the same modules render on the mobile "Status" tab (<768) and in the 768–1023 drawer;
   the >=1024 block keeps only the panel's own structure. Class-scoped, so pre-Status
   mobile screens (which never rendered these classes) are unchanged. */
.rp-card { background:#0E1014; border:1px solid #262C36; border-radius:2px; overflow:hidden; }
.rp-cardart { height:88px; background:#0C0E12 center/cover no-repeat; }
.rp-cardname { font-family:var(--serif); color:#E6D5A8; font-size:14px; padding:8px 10px 2px; }
.rp-cardlore { color:#83764F; font-style:italic; font-size:11.5px; padding:0 10px 8px; font-family:var(--serif); }
.rp-cardprog { color:#6A6E77; font-size:11px; padding:0 10px 8px; }
.rp-eqrow { display:flex; align-items:center; gap:8px; font-size:12.5px; color:#C3B48E; padding:3px 10px; }
.rp-card .rp-eqrow:last-child { padding-bottom:8px; }
.rp-dot { width:7px; height:7px; border-radius:50%; flex:0 0 auto; background:currentColor; }
.rp-modlabel { display:block; color:#6A6E77; font-size:9.5px; letter-spacing:.14em; margin-bottom:6px; }
.rp-mod { margin-bottom:14px; }
.rp-evrow { font-size:12px; color:#C3B48E; margin-bottom:4px; }
.rp-evrow b { color:#E6D5A8; display:block; }
.rp-evrow span { color:#83764F; font-size:11px; }
.rp-mod .rp-eqrow { padding:3px 0; }
/* 6c Character middle module: two equal material stat tiles (value serif over micro-label)
   + the right-aligned rarity tag on equipped-highlight rows. Chrome greys only — never a
   rarity hue (the .rp-dot beside the name is the rarity signal). */
.rp-mattiles { display:grid; grid-template-columns:1fr 1fr; gap:8px; }
.rp-mattile { border:1px solid #262C36; background:#0b0d11; border-radius:2px; padding:10px;
              display:flex; flex-direction:column; align-items:center; gap:2px; }
.rp-mattile b { font-family:var(--serif); font-weight:600; font-size:18px; color:#E6D5A8; }
.rp-mattile span { font-size:9px; letter-spacing:.12em; color:#6f7887; }
.rp-eqrow small { margin-left:auto; font-size:10px; color:#5a6270; text-transform:capitalize; }
/* Bounty mini-bar (6a module 6): 5px bar, SOLID BRONZE fill per the mock — not ember,
   never a rarity hue. The fill fraction is slot usage (my_active/max) — the only honest
   fraction the bounty system has. */
.rp-bountyrow { display:flex; justify-content:space-between; font-size:12px; color:#C3B48E;
                margin-bottom:6px; }
.rp-bountyage { color:#5a6270; font-size:10px; }
.rp-bountybar { height:5px; border:1px solid #2a3140; border-radius:2px; overflow:hidden; }
.rp-bountybar div { height:100%; background:#8A6A2E; }
.rp-quietline a, .rp-warcount a { color:#83764F; font-style:italic; text-decoration:none;
                                  font-size:11.5px; }
.rp-warname { font-family:var(--serif); color:#C3B48E; font-size:13px; }

/* 768–1023 (6a item 2: "frame fluid + right panel → drawer"): the tablet band gets the
   right panel as a fixed drawer behind a static toggle (#rpDrawerBtn lives in index.html —
   a strip-injected node would be wiped by every strip repaint). Below 768 the Status TAB
   carries these modules; >=1024 the persistent panel does — both hide the other's control. */
#rpDrawerBtn { display:none; }
@media (min-width: 768px) and (max-width: 1023.98px) {
  /* "frame fluid" (6a item 2): the band's frame tracks the viewport instead of pinning to
     the 480px phone column — capped at the content column's own desktop max (760). */
  body { max-width:min(92vw, 760px); }
  #rpDrawerBtn { display:block; position:fixed; top:9px; right:10px; z-index:60;
                 background:#12161d; color:#C3B48E; border:1px solid #2a3140;
                 border-radius:2px; padding:5px 9px; font-size:14px; cursor:pointer; }
  #rightpanel.open { display:flex; flex-direction:column; gap:18px; position:fixed;
                     top:0; right:0; bottom:0; width:340px; overflow-y:auto; z-index:55;
                     background:linear-gradient(180deg,#0b0d11,#08090c);
                     border-left:1px solid #171b23; padding:20px 18px; box-sizing:border-box;
                     box-shadow:-18px 0 40px -20px #000; }
  #tabs button[data-tab="status"] { display:none; } /* the drawer carries the modules here */
}
@media (min-width: 1024px) {
  #bg-zone { display:block; }
  /* Desktop layout pass (6a): widen the centered frame to 1320 and let the DIVIDERS (1px
     iron on the rail/panel) do the column separation — no gap. The gutters get a job (the
     margin wash, below), so they read as atmosphere, not void. */
  body {
    display:grid;
    grid-template-columns: 240px minmax(0,760px) 340px;
    grid-template-rows: auto auto 1fr;
    grid-template-areas:
      "header header header"
      "chips  chips  chips"
      "rail   content panel";
    column-gap:0;
    /* Item 6: margin on-scale (28→24, min-height pair kept in step), the leftover 1px
       ring dropped — the spec lift shadow stands alone. */
    max-width:1320px; margin:24px auto; min-height:calc(100vh - 48px);
    border:1px solid #262C36; border-radius:2px; position:relative;
    box-shadow:0 40px 120px -40px #000; }
  /* 2px bronze top-corner brackets on the frame (5a component language). */
  body::before, body::after { content:""; position:absolute; top:-1px; width:16px; height:16px;
    border-top:2px solid var(--bronze); pointer-events:none; z-index:6; }
  body::before { left:-1px; border-left:2px solid var(--bronze); }
  body::after  { right:-1px; border-right:2px solid var(--bronze); }
  #strip { grid-area:header; height:56px; box-sizing:border-box; }
  /* #chips collapses to zero height via the existing #chips:empty rule (above, T23)
     whenever no FTUE chip applies — that rule is untouched, so the "chips" grid row
     (auto-sized) collapses right along with it. */
  #chips { grid-area:chips; }
  /* Content column: flex-column pinned to the TOP (never vertically centered) so a short
     screen doesn't float; a .lorewisp band (below) at margin-top:auto absorbs the slack. */
  /* BOTTOM-CLIP FIX (ticket1 §6, part c resolved main-wins): the old
     min-height:calc(100vh - 56px) subtracted ONLY the strip — but the frame's 24px
     margins (×2), its 1px borders (×2), and the variable #chips row also eat viewport,
     so the constant overshot by ~50px and margin-top:auto anchors (lore band, warband)
     landed below the fold even WITH border-box. No static subtraction can equal the
     actual leftover; the grid already knows it: the body's 1fr row (bounded by the
     body's own min-height:calc(100vh - 48px)) stretches both columns to exactly the
     remaining height. min-height:0 hands the sizing to the grid — nothing to go stale.
     Guarded in test_css_computed.py::test_columns_never_clip_their_bottom_anchor. */
  main#screen { grid-area:content; max-width:760px; width:100%; margin:0; padding:24px;
                display:flex; flex-direction:column; min-height:0; box-sizing:border-box; }

  /* Left nav rail: additional declarations on the SAME #tabs/#tabs button/#tabs .on
     selectors styles.css already defines just above (the base mobile bottom-bar rule,
     directly before this media block — see FIX #7's own comment there for why its
     POSITION in the file matters) — these only win >=1024px via source order; the
     bottom-bar rule they override stays completely intact for every width below this
     breakpoint. */
  #tabs { grid-area:rail; flex-direction:column; align-items:stretch; justify-content:flex-start;
          border-top:0; border-right:1px solid #262C36; padding:18px 0; gap:2px; }
  #tabs button { flex:0 0 auto; text-align:left; padding:13px 22px; font-size:14px;
                 border-left:2px solid transparent; }
  /* Active nav (6a): ember text + 2px bronze left-inset + 12%-ember wash. */
  #tabs .on { color:#E7A83E; background:rgba(231,168,62,.12); border-left-color:var(--bronze); font-weight:700; }

  /* FIX #7 (P2, cert Day-1 batch T2): body-font pass on Camp + the world-set card —
     both already INHERIT body's own `font-family:var(--sans)` with
     the browser default (400/normal) weight; pinned here EXPLICITLY so the two
     surfaces can never silently drift apart (e.g. a future rule shadowing body's own
     font-family for just one of them). Scoped inside this >=1024px block per this
     ticket's own "mobile untouched" constraint — the owner's mismatch report was
     specifically against the desktop layout (this same nav-rail pass), and pinning it
     only here keeps the <1024px phone layout byte-for-byte what it was. Deeper visual
     confirmation (does this actually read as consistent by eye) needs a real browser —
     flagged for the Wave 4 pass, same as this branch's other pure-visual items. */
  /* Post-Pass-6 fix 5 (intro modal serif, D038): #worldcard .worldcard-lines p is
     REMOVED from this desktop sans reset — the world-set card's flavor copy is now on
     the serif world-voice register (see the .worldcard-lines rule below) on EVERY
     viewport; leaving it here would silently undo the serif on desktop only. */
  main#screen {
    font-family:var(--sans); font-weight:400;
  }

  #rightpanel { display:flex; flex-direction:column; gap:18px; grid-area:panel;
                border-left:1px solid #262C36; padding:20px; background:#0C0E12;
                min-height:0; box-sizing:border-box; }  /* grid-stretched — see main#screen's fix note */
  /* Right-panel modules (6a §3-⌄). #rightpanel is a flex-column with 18px gap, so each module
     just styles its own internals; warband bottom-anchors via margin-top:auto. */
  .rp-identity { text-align:center; }
  .rp-doll { display:flex; justify-content:center; margin-bottom:8px; }
  .rp-doll img { height:96px; filter:drop-shadow(0 6px 14px #00000088); }
  .rp-name { font-family:var(--serif); color:#E6D5A8; font-size:14px; margin-bottom:6px; }
  .rp-gearscore { text-align:center; color:#E7A83E; font-size:12px; letter-spacing:.08em; }
  .rp-gearscore b { display:block; font-size:19px; margin-top:3px; }
  .rp-resources { display:flex; flex-direction:column; gap:10px; }
  .rp-wallet { display:flex; flex-direction:column; gap:9px; }
  .rp-row { display:flex; align-items:center; gap:9px; font-size:14px; }
  .rp-row .ico { width:18px; text-align:center; }
  .rp-row .gold { color:#E6D5A8; font-weight:700; }
  .rp-row .dust { color:#C3B48E; font-weight:700; }
  .rp-energy { display:flex; align-items:center; gap:8px; font-size:12.5px; color:#83764F; }
  .rp-energy .ebar { flex:1; }
  .rp-hairline { height:1px; background:linear-gradient(90deg,transparent,#8A6A2E66,transparent); }
  /* Module internals (.rp-card/.rp-mod/tiles/bounty) are BASE-scoped now — the Status tab
     and the 768–1023 drawer render the same modules; only panel STRUCTURE lives here. */
  .rp-mod { margin-bottom:0; } /* the panel's own flex gap spaces modules; base margin is for Status/drawer flow */
  #tabs button[data-tab="status"] { display:none; } /* the persistent panel carries the modules here */
  .rp-warband { margin-top:auto; display:flex; justify-content:space-between; align-items:center;
                padding-top:12px; border-top:1px solid #1F242C; }
  .rp-warband .rp-modlabel { margin-bottom:0; }
  .rp-warcount { color:#83764F; font-size:12px; }

  /* --- Vertical-rhythm lore band (6a §6) ------------------------------------------------
     Pinned to the bottom of the content column (margin-top:auto) so a short screen stops
     "ending halfway" -- primary content stays TOP-pinned (main#screen is a flex-column, not
     centered) and this band is the only child that grows into the slack. An italic serif
     world-voice line over a faint active-zone wash (Camp) that masks up into the column so
     it reads as atmosphere fading in, never a hard panel. Below 1024 the base
     `.loreband{display:none}` keeps it off mobile entirely. */
  .loreband { display:flex; align-items:center; justify-content:center; margin-top:auto;
              min-height:110px; padding:20px 24px; box-sizing:border-box;
              border-top:1px solid #1F242C; background-size:cover; background-position:center;
              -webkit-mask-image:linear-gradient(to bottom, transparent, #000 44%);
              mask-image:linear-gradient(to bottom, transparent, #000 44%); }
  .loreband-line { font-family:var(--serif); font-style:italic; color:#6A6E77;
                   font-size:14px; line-height:1.4; text-align:center; letter-spacing:.01em;
                   text-shadow:0 1px 6px #000; }

  /* --- Multi-column matrix (6a §5) -----------------------------------------------------
     Keyed off #screen[data-tab] (set by render()) so each screen's OWN card container
     becomes the grid -- main#screen itself stays a flex-column (keeping the lore band's
     margin-top:auto working). Every rule lives inside this >=1024 block, so mobile stacks
     exactly as before. Forge is intentionally left single-column: its item "picker" is a
     <select>, not a list, so a 300px picker column would be a markup re-architecture this
     layout pass (grid/density/rhythm only) is scoped to avoid -- see DECISIONS. */
  /* Feats -> 2-col card grid. */
  #screen[data-tab="feats"] .featgrid { display:grid; grid-template-columns:1fr 1fr; gap:16px; align-items:start; }
  /* Character -> 1 column; the inventory grid fills the lower half at 8 tiles across (base 6). */
  #screen[data-tab="character"] .inv { grid-template-columns:repeat(8,1fr); }
  /* Map -> 2-col claim grid at the ticket's 16px gap (base .claimgrid is 12 at >=720). */
  #screen[data-tab="map"] .claimgrid { gap:16px; }
  /* Bounty -> 2 col: board 1fr, allies 320px aside. */
  .bountycols { display:grid; grid-template-columns:1fr 320px; gap:24px; align-items:start; }
  .bountycols .allysection { margin-top:0; }
  /* Arena -> 2 col: controls/record 1fr, ladder + team-brackets 320px aside. */
  .arenacols { display:grid; grid-template-columns:1fr 320px; gap:24px; align-items:start; }
  .arena-aside { display:flex; flex-direction:column; gap:18px; }
}

/* M1.6-F world events v1 (docs/superpowers/specs/m1.6-pve-depth.md): the Camp banner --
   "the world visibly changes without the player." Server-driven (GET /api/state's
   world_events list, worldEventBannerHtml in web/app.js); absent entirely when no event
   is active. Amber/gold accent ties it to the gold/dust economy readouts elsewhere in the
   strip rather than introducing a new hue. Flagged for Wave 4 visual polish (functional-
   first banner styling, not a bespoke illustration treatment). */
.worldevent { margin-bottom:10px; }
.weitem { background:#212730; border:1px solid #5a441a; border-radius:2px; padding:9px 12px;
          color:#E6D5A8; font-size:12.5px; font-weight:600; margin-bottom:6px; }
.weitem:last-child { margin-bottom:0; }

.sectionlabel { color:#83764F; letter-spacing:.1em; font-size:11px; margin-bottom:8px; }

/* Zone cards + tier chips (Camp screen) */
.zonelist { display:flex; flex-direction:column; gap:8px; }
.zcard { background:#0E1014; border:1px solid #262C36; border-radius:2px; padding:10px 12px; }
.zcard b { color:#E6D5A8; font-size:15px; }
.zcard .lvreq, .zcard .lock { color:#6A6E77; font-size:12px; }
.zcard-locked { opacity:.45; }
/* Zone-card art (delivered art/zone-<slug>-card.webp). Full-bleed cover under a NEUTRAL dark
   scrim (rule 2: text never on raw art; rule 4: never a rarity tint). ALL THREE hunting zones
   ship art (owner ruling 2026-07-16: Cinderpeak → Drowned Orchard, whose art was delivered);
   a 404 leaves the scrim over the base = the flat-panel look (rule 6). overflow clips to the
   2px radius. The 2026-07-15 P2's locked-state override rules are DROPPED per their own
   written plan ("when the third zone's art lands, drop these two rules and all three cards
   light up together") — the locked signal is the .zcard-locked base opacity, and every card,
   locked or open, paints its own ground. Contract: test_css_computed.py::
   test_every_zone_card_paints_its_own_art (real-browser computed styles — rule 5). */
.zcard[data-zone] { position:relative; overflow:hidden; }
.zcard[data-zone="blackmire_fen"], .zcard[data-zone="hollowspire_ruin"],
.zcard[data-zone="drowned_orchard"] {
  background-size:cover; background-position:center; }
.zcard[data-zone="blackmire_fen"] {
  background-image:linear-gradient(to top, rgba(8,9,12,.95) 0%, rgba(8,9,12,.74) 52%, rgba(8,9,12,.42) 100%), url("art/zone-blackmire-fen-card.webp"); }
.zcard[data-zone="hollowspire_ruin"] {
  background-image:linear-gradient(to top, rgba(8,9,12,.95) 0%, rgba(8,9,12,.74) 52%, rgba(8,9,12,.42) 100%), url("art/zone-hollowspire-ruin-card.webp"); }
.zcard[data-zone="drowned_orchard"] {
  background-image:linear-gradient(to top, rgba(8,9,12,.95) 0%, rgba(8,9,12,.74) 52%, rgba(8,9,12,.42) 100%), url("art/zone-drowned-orchard-card.webp"); }
.zcard[data-zone="blackmire_fen"] b, .zcard[data-zone="hollowspire_ruin"] b,
.zcard[data-zone="drowned_orchard"] b { text-shadow:0 1px 4px #000, 0 0 2px #000; }
.zcard[data-zone="blackmire_fen"] .tier, .zcard[data-zone="hollowspire_ruin"] .tier,
.zcard[data-zone="drowned_orchard"] .tier { background:rgba(10,11,14,.62); }
.tier { display:inline-block; border:1px solid #353C46; border-radius:2px; padding:4px 8px;
        margin:6px 6px 0 0; color:#B2A379; font-size:12px; cursor:pointer; }
.tier.lock { color:#6A6E77; border-style:dashed; cursor:default; }
.tier.sel { background:#1F242C; color:#E6D5A8; border-color:#E6D5A866; font-weight:700; }
/* FIX #13 (P2, owner playtest): "why climb" incentive hint on a locked tier chip
   (GET /api/zones' server-computed `incentive` field) — a second, distinctly-toned
   line INSIDE the same chip (display:block on a `small` inside the chip's own
   inline-block box), not a sibling element, so a hint-less chip's layout is
   byte-for-byte what it was before this fix. Violet ties it to the same
   progression/forge accent color used elsewhere (dust, reroll, forge nudge chip). */
.tierhint { display:block; margin-top:2px; color:#C3B48E; font-style:italic; font-size:10.5px; }

/* Anticipation Gradient #2 (threat legibility, docs/superpowers/specs/anticipation-
   gradient.md): a Favored/Even/Risky chip riding inside the tier chip (app.js's
   threatChipHtml) and, identically, inside the HUNT button label (huntLabel()) — never
   a raw win_chance %. Three distinct treatments per the brief ("favored=calm,
   risky=warning"): favored reads as a calm, settled green; even a neutral, unremarkable
   gray-blue (deliberately the least visually loud of the three); risky a warm amber-red
   warning, tying it to the same red family HUNT/DEFEAT already use elsewhere. Functional-
   first chip styling (flat pill, no glow/animation) — a bespoke juice pass is a Wave-4
   visual-polish item, not built here. */
.threatchip { display:inline-block; margin-left:2px; padding:1px 6px; border-radius:2px;
              font-size:9.5px; font-weight:700; letter-spacing:.03em; vertical-align:middle; }
.threat-favored { background:#12281f; color:#E6D5A8; border:1px solid #262C3655; }
.threat-even { background:#12161C; color:#83764F; border:1px solid #353C46; }
.threat-risky { background:#2a1414; color:#C4553E; border:1px solid #C4553E55; }

/* Anticipation Gradient C (zone heat, ticket #25): a small filling meter under a zone
   card's tier row (app.js's zoneHeatHtml, GET /api/zones' `heat:{value,max}`) — a
   per-zone, VISIBLE buildup cue (see the spec's own "honor the anti-pity line": global
   pity was banned for being invisible AND global; this is neither). Reuses the same
   flat filled-bar language as .ebar/.xpbar/.enemyhpbar above rather than inventing a
   fourth bar treatment. `.zoneheat-near` (>=80% full, app.js) swaps the fill to a
   warmer gradient and surfaces the "the fen stirs" cue text — the meter itself stays
   the same height/shape throughout, only its color and the cue line change. Functional-
   first — a bespoke ambient pulse/glow as the meter nears full is a Wave-4 visual-
   polish item, not built here. */
.zoneheat { margin-top:8px; }
.zoneheatbar { height:5px; background:#191D24; border-radius:3px; overflow:hidden; }
.zoneheatfill { height:100%; background:linear-gradient(90deg,#2E343E,#8A6A2E); }
.zoneheat-near .zoneheatfill { background:linear-gradient(90deg,#C4553E,#D6763A); }
.zoneheatcue { display:block; margin-top:3px; color:#C4553E; font-style:italic; font-size:10.5px; }

/* M1.6-D (discovery silhouettes, docs/superpowers/specs/m1.6-pve-depth.md): per-zone
   "Bases: x/y" + a chip per base (discovered = real name, undiscovered = "???"). Shared
   verbatim by Camp zone cards (zoneBasesHtml, inside .zcard) and the Craft tab's
   per-zone DISCOVERY section (.zdiscovery) — same server payload (GET /api/zones'
   bases/discovered_count/total), same markup, so the two screens can never visually
   disagree on what's discovered. Flagged for Wave 4 visual polish (functional-first
   chip styling, not a bespoke illustration treatment). */
.zbases { margin-top:8px; }
.zbaseslabel { color:#6A6E77; font-size:11px; letter-spacing:.04em; }
.zbaselist { display:flex; flex-wrap:wrap; gap:5px; margin-top:5px; }
.basechip { display:inline-block; border-radius:2px; padding:3px 7px; font-size:10.5px;
            border:1px solid #262C36; }
.basechip.disc { color:#B2A379; background:#12161C; }
.basechip.undisc { color:#2E343E; background:#0d1220; border-style:dashed;
                    letter-spacing:.1em; }
.zdiscovery { margin-top:12px; }
.zdiscovery-head { margin-bottom:2px; }
.zdiscovery-head b { color:#C3B48E; font-size:13px; }

/* M1.6-B hunt stances, REACHABILITY pass (Camp hunt card, app.js's stanceSelectorHtml):
   a compact 3-way segmented control -- "like a radio group" per the owner spec -- sitting
   between the zone list and the HUNT button. Visual language deliberately mirrors the
   Forge screen's own .tabs2 sub-tab nav (same flat pill / gold-on-selected treatment) so
   the app doesn't invent a third segmented-control style, just scaled down (tighter
   padding, a second stacked line for the one-line trade copy) to read as "compact," not
   a full tab bar. Functional-first; a bespoke stance-specific illustration/icon treatment
   is a Wave-4 visual-polish item, not built here. */
.stancesel { display:flex; gap:6px; margin-top:12px; }
.stanceseg { flex:1; text-align:center; padding:7px 4px; border-radius:2px; background:#0E1014;
             border:1px solid #262C36; color:#83764F; cursor:pointer; }
.stanceseg.on { background:#1F242C; border-color:#3A4250; }
.stancename { font-size:12px; font-weight:700; letter-spacing:.03em; color:#B2A379; }
.stanceseg.on .stancename { color:#E6D5A8; }
.stancetrade { margin-top:2px; font-size:9.5px; line-height:1.25; color:#6A6E77; }
.stanceseg.on .stancetrade { color:#C4A96A; }

/* Ratified breach fix (5a): the primary CTA is FORGED BRONZE, not red. Red is Mythic-
   only; the old red primary was squatting on a rarity hue. This is the shared primary —
   HUNT / HUNT AGAIN / CHALLENGE / ENTER all inherit it. Ember glow per the glow law. */
/* Primary CTA — forged EDGE + GLOW on a dark fill (owner: not a solid gold fill). Cold
   iron body, 1px bronze edge, ember text + ember glow. The whole shared primary family
   (HUNT / HUNT AGAIN / CHALLENGE / ENTER / reroll / escalation) inherits this. */
.bigbtn { width:100%; margin-top:14px; background:linear-gradient(180deg,#171B22,#0E1014);
          color:var(--ember); text-align:center; border:1px solid var(--bronze); border-radius:2px;
          padding:14px 0; font-weight:700; font-size:16px; letter-spacing:.05em;
          box-shadow:var(--ember-glow), inset 0 1px 0 #8A6A2E22; }
.bigbtn:disabled { background:var(--inset); color:var(--muted); border-color:var(--iron); box-shadow:none; }

/* ---- FIX #20 (P2, owner playtest, design §9): energy wall = retention-exit HANDOFF,
   not a dead end. renderEnergyWall() (app.js) replaces the Camp screen's zone list/
   HUNT button with this surface whenever energy < the hunt cost -- a live countdown
   (app.js's startEnergyWallTimer, display-only) plus a short forward-looking goals
   list, so the player has a reason to return instead of bouncing off a bare toast. */
.energywall { background:#0E1014; border:1px solid #262C36; border-radius:2px;
              padding:16px 14px; text-align:center; }
.ewtitle { color:#C3B48E; font-size:14px; font-weight:600; margin-bottom:12px; }
.ewcountdown { display:flex; flex-direction:column; gap:6px; margin-bottom:16px;
               color:#83764F; font-size:13px; }
.ewcountdown b { color:#E6D5A8; font-size:15px; }
.ewfull b { color:#C3B48E; }
.ewgoals-label { margin-top:4px; }
.ewgoals { list-style:none; margin:0; padding:0; display:flex; flex-direction:column; gap:8px;
           text-align:left; }
.ewgoals li { background:#0A0B0E; border-radius:2px; padding:0; font-size:12.5px;
              color:#B2A379; border-left:2px solid #262C3688; }
/* T2.8 (energy-wall upgrades): the goal list items are now deep-links (renderEnergyWall/
   wireEnergyWallLinks, app.js) — the anchor fills the li so the whole row stays tappable,
   same footprint as the old plain-text li it replaces. */
.ewgoals li a.ewlink { display:block; padding:9px 12px; color:#B2A379; text-decoration:none; }
.ewgoals li a.ewlink:hover, .ewgoals li a.ewlink:active { color:#E6D5A8; }
/* "The Forge never sleeps" — energy-free verb row (dust/scrap/unbound-loot holders
   only; forgeNeverSleepsOptions(), app.js), styled as pill links in the same violet
   forge accent .tierhint/.forgenudge already use. */
.ewsleep-label { margin-top:16px; }
.ewsleep { display:flex; flex-wrap:wrap; gap:8px; text-align:left; margin-bottom:4px; }
.ewsleepverb { display:inline-block; border:1px solid #262C3655; border-radius:20px;
               padding:6px 12px; color:#C3B48E; font-size:12px; text-decoration:none; }
.ewsleepverb:hover, .ewsleepverb:active { background:#262C3622; }

.stub { text-align:center; color:#6A6E77; font-size:13px; padding:48px 12px; }

/* Ratified breach fixes (design pass, sheet 5a). VICTORY/DEFEAT are world-voice, not
   rarity hues: VICTORY in ember serif, DEFEAT in ash-parchment serif (defeat is absence,
   not a red screamer — red is Mythic-only). The serif comes from .banner below. */
.win { color:var(--ember); font-weight:700; letter-spacing:.15em; }
.loss { color:var(--parchment); opacity:.72; font-weight:700; }
.log { font-family:Consolas,monospace; font-size:12px; color:var(--body);
       border-left:2px solid var(--iron); padding-left:8px; }
.err { color:var(--danger); }
.item { border:1px solid var(--iron); border-radius:2px; padding:8px; cursor:pointer; }
.r-common{border-color:var(--r-common)}.r-uncommon{border-color:#5AC67A;color:#5AC67A}
.r-rare{border-color:#4A9EFF;color:#4A9EFF}.r-epic{border-color:#A56BFF;color:#A56BFF}
.r-legendary{border-color:#E0B25A;color:#E0B25A}.r-mythic{border-color:#E5484D;color:#E5484D}
/* Post-Pass-6 fix 3 (feed gets its colors, D038): the feed's item-name bold sits under
   `.feedtext b` (class+element specificity), which OUT-RANKS a bare .r-* class — so the
   tint is re-declared here at two-class specificity for the feed context. Kept adjacent
   to the palette above: retune a rarity color in both lines or it drifts. Common stays
   neutral (inherits .feedtext b's #E6D5A8) — the correct no-tint. */
.feedtext b.r-uncommon{color:#5AC67A}.feedtext b.r-rare{color:#4A9EFF}
.feedtext b.r-epic{color:#A56BFF}.feedtext b.r-legendary{color:#E0B25A}
.feedtext b.r-mythic{color:#E5484D}

/* Toast — error surface (e.g. insufficient_energy), replaces unhandled rejections.
   FIX #13 (P2): z-index raised 50 -> 200 (above #reveal's 100 AND #screenflash's
   150) — a tier-unlock toast now fires from inside hunt(), in the same synchronous
   turn that immediately opens the full-screen results reveal (#reveal, z-index:100),
   so the toast must out-rank it or it would paint invisibly underneath. pointer-
   events stays `none` unconditionally (below), so this never steals a tap from the
   reveal's own tap-anywhere-to-skip handler. */
#toast { position:fixed; left:50%; bottom:70px; transform:translateX(-50%) translateY(12px);
         max-width:90%; background:#191D24; border:1px solid #C4553E; color:#C4553E;
         padding:10px 16px; border-radius:2px; font-size:13px; text-align:center;
         opacity:0; pointer-events:none; transition:opacity .2s, transform .2s; z-index:200; }
#toast.show { opacity:1; transform:translateX(-50%) translateY(0); }
/* T23: success confirmations (buy/sell/claim/login) share this element but must not
   read as an error — see toast()'s own comment in app.js. */
#toast.ok { border-color:#E6D5A8; color:#E6D5A8; }

/* ---- Results Reveal (T21) — the game's primary dopamine moment (SPEC.md §6/§7) ----
   Keyframes slam/fadein/popin/flipin/tease/xpgrow/flashg/glowb/glowo/pulse are lifted
   VERBATIM from the owner-approved mockup (docs/mockups/doll-v3-and-reveal.html,
   verdict "2B reveal as shown"), except xpgrow's from/to widths, which are necessarily
   parameterized via CSS custom properties (--xp-from/--xp-to, set by app.js per hunt)
   since the mockup's 8%/62% were fixed demo numbers, not real per-hunt data. glowp
   (epic/purple) and glowr (mythic/red) extend the glowb/glowo pattern to the two
   rarities the mockup didn't demo, reusing the same rarity colors as the .r-<rarity>
   classes above. Triggers are JS-toggled classes (.play / .tease-*), not baked-in
   animation-delay, so "tap anywhere" (app.js: skip()) can cancel mid-flight. */

@keyframes slam { from{opacity:0;transform:scale(2.2)} to{opacity:1;transform:scale(1)} }
@keyframes fadein { to{opacity:1} }
@keyframes popin { from{opacity:0;transform:scale(.7)} to{opacity:1;transform:scale(1)} }
@keyframes flipin { from{opacity:0;transform:rotateX(88deg) translateY(8px)} to{opacity:1;transform:rotateX(0) translateY(0)} }
@keyframes tease { 0%{opacity:.25;transform:translateX(0)} 25%{transform:translateX(-2px)} 50%{transform:translateX(2px)} 75%{transform:translateX(-1px)} 100%{opacity:.25;transform:translateX(0)} }
@keyframes xpgrow { from{width:var(--xp-from,8%)} to{width:var(--xp-to,62%)} }
/* FIX #12 (P2, owner playtest "a DISTRACTED player cannot miss it"): flashg/glowb/
   glowo/glowp/glowr are INTENSIFIED here (bigger blur + spread + peak opacity than the
   original owner-approved mockup values, mythic now double-blooms) — an explicit
   owner directive to turn these up, not a drift from the approved design. Shared by
   both #reveal's loot cards and Forge's .frcard landings (below), so both screens'
   celebratory beats get the boost. */
@keyframes flashg { 0%{box-shadow:0 0 0 #5AC67A00} 25%{box-shadow:0 0 26px 4px #5AC67Acc} 100%{box-shadow:0 0 0 #5AC67A00} }
@keyframes glowb { 0%{box-shadow:0 0 0 #4A9EFF00} 25%{box-shadow:0 0 34px 6px #4A9EFFee} 100%{box-shadow:0 0 10px #4A9EFF44} }
@keyframes glowo { 0%{box-shadow:0 0 0 #E0B25A00} 22%{box-shadow:0 0 40px 8px #E0B25Af2} 100%{box-shadow:0 0 12px #E0B25A55} }
@keyframes pulse { 0%,100%{transform:scale(1)} 50%{transform:scale(1.03)} }
@keyframes glowp { 0%{box-shadow:0 0 0 #A56BFF00} 24%{box-shadow:0 0 36px 7px #A56BFFee} 100%{box-shadow:0 0 11px #A56BFF44} }
@keyframes glowr { 0%{box-shadow:0 0 0 #E5484D00} 20%{box-shadow:0 0 46px 10px #E5484Dff} 45%{box-shadow:0 0 18px 2px #E5484D88} 65%{box-shadow:0 0 44px 9px #E5484Df5} 100%{box-shadow:0 0 13px #E5484D55} }
@keyframes vigfade { 0%{opacity:0} 60%{opacity:.72} 100%{opacity:0} }
/* FIX #30 (P2): the hunt-3 scripted-rare ceremony's own brief screen-dim fade — see
   #vignette.ceremony.play below. */
@keyframes ceremonyfade { 0%{opacity:0} 55%{opacity:.6} 100%{opacity:0} }
/* FIX #12: the loot card's own "border-pulse" (a scale bump, layered onto the card's
   existing flipin/glow* animation — see the .play.r-<rarity> rules below), scaled by
   rarity via iteration count/duration (cardpulse for uncommon/rare/epic, the bigger
   cardpulseBig for legendary/mythic). */
@keyframes cardpulse { 0%,100%{transform:scale(1)} 50%{transform:scale(1.08)} }
@keyframes cardpulseBig { 0%,100%{transform:scale(1)} 30%{transform:scale(1.14)} 60%{transform:scale(1.02)} 80%{transform:scale(1.1)} }
@keyframes ceremonyburst { 0%{opacity:0;transform:scale(.5) rotateX(88deg)} 55%{opacity:1;transform:scale(1.18) rotateX(0)} 75%{transform:scale(.96)} 100%{opacity:1;transform:scale(1)} }

/* T2.1 (persistent header on Results): top padding grown from 24px to 60px so
   .reveal-inner's own centered content never starts underneath the fixed .reveal-strip
   clone below (~44px tall) — a plain clearance bump, not a restructure of the reveal
   grammar itself. Every other #reveal rule is unchanged. */
#reveal { position:fixed; inset:0; z-index:100; background:#08090C; color:#C3B48E;
          display:flex; align-items:center; justify-content:center;
          padding:60px 16px 24px; overflow-y:auto; }
#reveal .reveal-inner { position:relative; z-index:2; width:100%; max-width:380px; }
/* CLAIM-ALL summary (owner defect 2026-07-17): the panel borrowed .reveal-panel's NAME,
   but every overlay rule above is #reveal-ID-bound — so it mounted as a bare static
   body child and rendered in the frame GUTTER, below the fold. It is a takeover: the
   same overlay grammar, ceremony column centered. (The chest-opening ceremony rework
   later replaces the INTERIOR — the mount is right either way.) Computed-guarded in
   tests/test_css_computed.py: content never renders in the frame margins. */
#claimAllSummary { position:fixed; inset:0; z-index:100; background:rgba(8,9,12,.95);
                   color:#C3B48E; display:flex; align-items:center; justify-content:center;
                   padding:60px 16px 24px; overflow-y:auto; }
#claimAllSummary .reveal-inner { position:relative; z-index:2; width:100%; max-width:380px; }

/* STAGED REVEAL SURROUND (ticket2 §10a, owner-approved on the §11R lock pair) — the 4c
   warm fill + red watermark are SUPERSEDED: "watermark = TRUE texture, cool slate...
   NEVER a warm/red tone (the loud red was the bug)". Desktop only (<1024 collapses to
   the column-only reveal — the mobile ceremony is untouched). The ceremony column
   itself stays ~330px centered and byte-identical. */
.rv-watermark { display:none; }
.rv-stage { display:none; }
@media (min-width:1024px) {
  /* §10a.1 — the DEEP vignette + inset edge falloff (cool, never warm). */
  #reveal { background:
      radial-gradient(75% 70% at 50% 30%, #0e1622 0%, #090d14 55%, #06080b 100%); }
  #reveal::after { content:''; position:absolute; inset:0; z-index:1; pointer-events:none;
     box-shadow:inset 0 0 260px 80px rgba(0,0,0,.72); }
  #reveal .reveal-inner { max-width:330px; }
  /* §10a.3 — the watermark as TRUE texture: cool slate, <=4%, one clipped line. */
  .rv-watermark { display:block; position:absolute; z-index:0; inset:0; pointer-events:none;
     display:flex; align-items:center; justify-content:center; overflow:hidden;
     font-family:var(--serif); font-weight:600; font-size:clamp(120px,20vw,300px);
     letter-spacing:.04em; line-height:.9; white-space:nowrap; text-align:center;
     color:#96A8B4; opacity:.035; }
  .rv-stage { display:block; }
}
/* §10a.2/4/5 + §11 — the stage: wash, answering glow, grounding band, the two actors.
   All SURROUND (z-index 0, behind .reveal-inner's z2), pointer-inert. */
.rv-stage { position:absolute; inset:0; z-index:0; overflow:hidden; pointer-events:none; }
.rv-wash { position:absolute; left:50%; top:44%; transform:translate(-50%,-50%);
  width:1100px; height:72%; background-size:cover; background-position:center;
  opacity:.08; filter:blur(30px) saturate(.5); }
@media (min-width:2000px) { .rv-wash { opacity:.11; } }
@media (min-width:2560px) { .rv-wash { opacity:.14; } }
/* §10a.4 — the answering glow: hue-neutral steel, opacity tracks the reveal's tier
   (uncommon is the floor; commons/none stay at base). PROVEN neutral — the rendered-
   pixel channel-spread assertion lives in tests/test_css_computed.py. */
.rv-answerglow { position:absolute; left:50%; top:50%; transform:translate(-50%,-50%);
  width:760px; height:760px; border-radius:50%;
  background:radial-gradient(circle, rgba(150,170,190,var(--glow-a,.05)) 0%, transparent 62%); }
.rv-stage.tier-rare { --glow-a:.06; }
.rv-stage.tier-epic { --glow-a:.07; }
.rv-stage.tier-legendary { --glow-a:.075; }
.rv-stage.tier-mythic { --glow-a:.08; }
/* §10a.5 — the grounding band: the column sits ON the marsh. */
.rv-ground { position:absolute; left:0; right:0; bottom:0; height:33%;
  background:linear-gradient(180deg, transparent 0%, rgba(10,14,18,.5) 60%, rgba(6,8,11,.9) 100%); }
.rv-waterline { position:absolute; left:8%; right:8%; bottom:29%; height:1px;
  background:linear-gradient(90deg, transparent, rgba(150,168,180,.14), transparent); }
.rv-mist { position:absolute; left:0; right:0; bottom:18%; height:120px;
  background:radial-gradient(60% 100% at 40% 100%, rgba(120,140,150,.05), transparent),
             radial-gradient(50% 100% at 75% 100%, rgba(120,140,150,.04), transparent);
  animation:rvmist 18s ease-in-out infinite alternate; }
@keyframes rvmist { from { transform:translateX(-14px); } to { transform:translateX(14px); } }
/* §11 — the actors: treatment, not a repaint. Hidden until CAST (both or neither). */
.rv-actor { position:absolute; bottom:16%; width:30vh; display:none; }
.rv-stage.cast .rv-actor { display:block; }
.rv-bust { position:relative; }
.rv-bust img { width:100%; display:block; opacity:.9;
  filter:brightness(var(--actor-exposure,.2)) contrast(1.15) saturate(.6);
  -webkit-mask-image:radial-gradient(75% 80% at 30% 45%, #000 40%, transparent 78%);
  mask-image:radial-gradient(75% 80% at 30% 45%, #000 40%, transparent 78%); }
.rv-stage.exposure-elite { --actor-exposure:.4; }
.rv-stage.exposure-boss { --actor-exposure:.6; }  /* reserved — the boss reuse */
.rv-eyes { position:absolute; inset:0; mix-blend-mode:screen;
  background:radial-gradient(circle at var(--ex,50%) var(--ey,40%),
    rgba(231,168,62,.6) 0%, rgba(231,168,62,.22) 2.5%, transparent 6%); }
.rv-enemy { left:5%; }
.rv-ally { right:5%; width:auto; }
.rv-ally .rv-bust img { height:56vh; width:auto; margin-left:auto; transform:scaleX(-1);
  filter:brightness(var(--ally-exposure,.28)) contrast(1.15) saturate(.6);
  -webkit-mask-image:radial-gradient(75% 80% at 70% 45%, #000 40%, transparent 78%);
  mask-image:radial-gradient(75% 80% at 70% 45%, #000 40%, transparent 78%); }
.rv-actorchip { margin-top:10px; text-align:center; font-size:12px; color:#8a8f99;
  letter-spacing:.06em; }
.rv-actorchip b { font-family:var(--serif); color:#C3B48E; letter-spacing:.08em; }
.rv-slain { text-decoration:line-through; color:#6A6E77; font-family:var(--serif);
  font-size:14px; letter-spacing:.1em; }
.rv-hpdead { width:120px; height:4px; margin:6px auto 0; background:#14181f;
  border:1px solid #262C36; border-radius:2px; }
.rv-hpdead i { display:block; width:0%; height:100%; background:#C4553E; }

/* T2.1: the wallet/energy header, visible over the Results screen. #reveal is opaque
   and full-screen (z-index:100 above the real #strip, which is an unpositioned body
   child), so today the wallet is fully hidden during the reveal — this is a compact
   CLONE of #strip's own chrome (SAME markup, via app.js's buildStripHtml(), shared
   with renderStrip()), pinned fixed to the viewport top rather than nested inside
   .reveal-inner so it can never be touched by scheduleReveal's staggered bloom timing.
   z-index:5 — above #vignette(1)/.mute-toggle(3) so it reads over the reveal backdrop,
   below #screenflash(150) so the existing full-screen flash/shake beats still sweep
   over it exactly like they do over everything else in #reveal. */
.reveal-strip { position:fixed; top:0; left:0; right:0; z-index:5;
                display:flex; align-items:center; gap:10px; justify-content:space-between;
                padding:10px 14px; background:#0E1014; border-bottom:1px solid #262C36;
                font-size:14px; }
.reveal-strip .ebar { flex:1; height:7px; background:#191D24; border-radius:4px; overflow:hidden; margin:0 2px; }
.reveal-strip .ebar>div { height:100%; background:linear-gradient(90deg,#8A6A2E,#E7A83E); }
.reveal-strip .gold { color:#E6D5A8; }
.reveal-strip .dust { color:#C3B48E; }

/* mythic-only: "a div#vignette with a radial gradient fading the screen for 900ms
   before a mythic card" (brief). #2A0A10 reuses the Mythic swatch background from
   docs/mockups/gamebreaking-and-lock.html's rarity ladder. */
#vignette { position:absolute; inset:0; z-index:1; pointer-events:none; opacity:0;
            background:radial-gradient(ellipse at center, transparent 32%, #3b0f16cc 100%); }
#vignette.play { animation:vigfade 1.5s ease forwards; }
#reveal.done #vignette { opacity:0 !important; animation:none !important; }
/* FIX #30 (P2): the hunt-3 scripted-rare's own screen-dim — warm gold, not mythic's
   dark red, and a single "brief" fade (.9s) rather than mythic's long tease (1.5s).
   Higher specificity (id + 2 classes) than the base #vignette.play rule above, so it
   always wins regardless of source order. */
#vignette.ceremony.play { animation:ceremonyfade .9s ease forwards;
                           background:radial-gradient(ellipse at center, transparent 30%, #3b2f0fcc 100%); }

/* ---- FIX #12 (P2, owner playtest): unmissable Uncommon+ landings -----------------
   Acceptance test: "a DISTRACTED player cannot miss it." A per-card glow alone is easy
   to miss if the player's eyes aren't already on that exact card — #screenflash is a
   full-viewport flash (always in view, regardless of scroll/gaze), triggered
   alongside the card's own landing and scaled by rarity (mild single pulse for
   Uncommon, up through a louder double-pulse for Mythic); .reveal-inner additionally
   SHAKES for legendary/mythic, the two "overwhelming" tiers. Commons get neither —
   app.js's scheduleCard() only ever adds a flash- and shake- class for SFX_RARITIES
   (uncommon+), so "commons stay quiet" is structural, not just a missing CSS rule.
   flash-ceremony (FIX #30) is the scripted-rare's own distinct gold flash. */
#screenflash { position:fixed; inset:0; z-index:150; pointer-events:none; opacity:0; }
#reveal.done #screenflash { opacity:0 !important; animation:none !important; }
#screenflash.flash-uncommon { animation:flashscreen-uncommon .4s ease-out; }
#screenflash.flash-rare { animation:flashscreen-rare .5s ease-out; }
#screenflash.flash-epic { animation:flashscreen-epic .6s ease-out; }
#screenflash.flash-legendary { animation:flashscreen-legendary .75s ease-out; }
#screenflash.flash-mythic { animation:flashscreen-mythic 1.1s ease-out; }
#screenflash.flash-ceremony { animation:flashscreen-ceremony .9s ease-out; }
/* T2.5 (QA-B5): the level-up card's own gold flash — distinct trigger from the loot-
   rarity ladder above (scheduleReveal/scheduleDefeatContent add this class directly,
   never scheduleCard), same gold tone as .levelup-card itself so the flash and the
   card it announces read as one event. */
#screenflash.flash-levelup { animation:flashscreen-levelup .6s ease-out; }
@keyframes flashscreen-uncommon { 0%{opacity:0;background:#5AC67A50} 18%{opacity:.5} 100%{opacity:0} }
@keyframes flashscreen-rare { 0%{opacity:0;background:#4A9EFF60} 16%{opacity:.6} 100%{opacity:0} }
@keyframes flashscreen-epic { 0%{opacity:0;background:#A56BFF75} 15%{opacity:.7} 100%{opacity:0} }
@keyframes flashscreen-legendary { 0%{opacity:0;background:#E0B25A88} 12%{opacity:.82} 100%{opacity:0} }
@keyframes flashscreen-mythic { 0%{opacity:0;background:#E5484D99} 10%{opacity:.92} 38%{opacity:.35} 55%{opacity:.78} 100%{opacity:0} }
@keyframes flashscreen-ceremony { 0%{opacity:0;background:#E6D5A880} 14%{opacity:.85} 42%{opacity:.32} 62%{opacity:.7} 100%{opacity:0} }
@keyframes flashscreen-levelup { 0%{opacity:0;background:#E6D5A870} 20%{opacity:.75} 100%{opacity:0} }

@keyframes shake-legendary { 0%,100%{transform:translate(0,0)} 20%{transform:translate(-3px,2px)} 40%{transform:translate(3px,-2px)} 60%{transform:translate(-2px,-2px)} 80%{transform:translate(2px,2px)} }
@keyframes shake-mythic { 0%,100%{transform:translate(0,0)} 15%{transform:translate(-6px,3px)} 30%{transform:translate(6px,-3px)} 45%{transform:translate(-5px,-4px)} 60%{transform:translate(5px,4px)} 75%{transform:translate(-3px,3px)} 90%{transform:translate(3px,-2px)} }
.reveal-inner.shake-legendary { animation:shake-legendary .4s ease; }
.reveal-inner.shake-mythic { animation:shake-mythic .55s ease; }
#reveal.done .reveal-inner { transform:none !important; animation:none !important; }

/* M1.9 item 23 — REVEAL v3 (D034): the TOP-rarity (legendary/mythic ONLY) CHARGE → TAKEOVER
   overlay. Inert by default; scheduleCard adds .charging-<rarity> during the build-up, then
   swaps to .takeover-<rarity> at the burst. Sits ABOVE the reveal backdrop/content (z 160,
   over #screenflash's 150) so a distracted player cannot miss the moment, but pointer-events
   stay off so it never traps a tap (the reveal stays skippable). A rare/epic/uncommon drop
   never touches this element, so the takeover is exclusive to the two top rarities. */
.rv-takeover { position:fixed; inset:0; z-index:160; pointer-events:none; opacity:0; }
#reveal.done .rv-takeover { opacity:0 !important; animation:none !important; }
/* CHARGE: a slow radial swell that grows toward the burst — legendary gold, mythic rose. */
.rv-takeover.charging-legendary { animation:charge-legendary 1s ease-in forwards;
  background:radial-gradient(circle at 50% 46%, #E0B25A33 0%, #E0B25A00 60%); }
.rv-takeover.charging-mythic { animation:charge-mythic 1.5s ease-in forwards;
  background:radial-gradient(circle at 50% 46%, #E5484D40 0%, #E5484D00 62%); }
@keyframes charge-legendary { 0%{opacity:0} 100%{opacity:.55} }
@keyframes charge-mythic { 0%{opacity:0} 70%{opacity:.45} 100%{opacity:.72} }
/* TAKEOVER: the burst — a full-screen flash-and-recede radial that momentarily OWNS the
   screen, then clears to reveal the landed card. Mythic runs longer/brighter than legendary. */
.rv-takeover.takeover-legendary { animation:takeover-legendary .95s ease-out forwards;
  background:radial-gradient(circle at 50% 46%, #fde68a 0%, #E0B25A 30%, #E0B25A00 72%); }
.rv-takeover.takeover-mythic { animation:takeover-mythic 1.3s ease-out forwards;
  background:radial-gradient(circle at 50% 46%, #ffe4e6 0%, #F0736F 32%, #E5484D00 74%); }
@keyframes takeover-legendary { 0%{opacity:.55;transform:scale(.7)} 14%{opacity:.9;transform:scale(1)} 100%{opacity:0;transform:scale(1.15)} }
@keyframes takeover-mythic { 0%{opacity:.72;transform:scale(.6)} 12%{opacity:.96;transform:scale(1)} 40%{opacity:.5} 60%{opacity:.85} 100%{opacity:0;transform:scale(1.2)} }
/* the charging card itself gets a subtle pre-burst pulse (over the existing .tease-* shake). */
.dr.charging-legendary, .dr.charging-mythic { animation:card-charge .9s ease-in-out; }
@keyframes card-charge { 0%,100%{filter:brightness(1)} 50%{filter:brightness(1.4)} }

/* T2.1: top nudged from 14px to 54px — clears the new fixed .reveal-strip header
   (~44px tall) above it instead of sitting underneath/behind it. */
.mute-toggle { position:absolute; top:54px; right:16px; z-index:3; width:34px; height:34px;
               background:#191D24; border:1px solid #353C46; border-radius:2px;
               color:#C3B48E; font-size:15px; line-height:1; }

.rv-el { opacity:0; }
/* Skip: forces every element to its resting state immediately, and stops any
   in-flight animation dead rather than letting it keep running invisibly. */
#reveal.done .rv-el { opacity:1 !important; transform:none !important; animation:none !important; }
#reveal.done .xpfill { width:var(--xp-to,62%) !important; }

/* Anticipation Gradient A (omens, docs/superpowers/specs/anticipation-gradient.md):
   the held-breath PRE-reveal beat (app.js's omenBeatHtml/scheduleReveal — the omen
   line pops immediately, then the whole normal cascade below is nested OMEN_PAUSE_MS
   deeper, see that function's own comment). rv-el/.play/#reveal.done are all inherited
   for free (generic .rv-el rules above already cover any element with that class, same
   reuse .tierunlock below relies on). Amber/italic ties it to the same "ambient
   flavor text" tone .worldevent/.weitem already use for the Camp banner, distinct from
   .enclabel's violet (an encounter is a CHOICE; an omen is a passive foreshadow).
   Functional-first fade-in only — a bespoke held-breath juice pass (a brief screen-dim,
   a low ambient tone) is a Wave-4 visual-polish item, not built here. */
.omen-beat { text-align:center; margin-bottom:10px; }
.omen-beat.play { animation:fadein .4s ease forwards; }
/* M1.9 item 23 (D034, serif pass): the omen line is an ambient dark-fantasy FORESHADOWING
   beat — a lore/voice surface. data/voices.toml's own register note names data/omen.toml as
   part of the same evocative serif voice, but this line shipped in the default sans (italic
   only), out of step with .voiceline/.worldvoice/.npcname. Brought onto the SAME serif
   register so every world/lore voice reads as one consistent register. */
.omenline { color:#C3B48E; font-family:var(--serif); font-size:13px;
            font-style:italic; letter-spacing:.02em; }

/* VICTORY/DEFEAT are world-voice: display serif (5a type law). .win/.loss carry the
   colour (ember / ash-parchment) — the serif register lives here so both moods read as
   one screen. 30px masthead scale (5a). */
.banner { text-align:center; font-family:var(--serif); font-size:30px; font-weight:600;
          letter-spacing:.14em; margin-bottom:6px; }
.banner.play { animation:slam .45s cubic-bezier(.2,1.6,.4,1) forwards; }
/* Zone banner (hunt-transition/zone-header, art/zone-<slug>-banner.png). A slim wide strip at
   the top of the ceremony; the zone name sits at the bottom over a NEUTRAL dark scrim baked into
   the inline background. No base color, so a 404 shows only the faint scrim (rule 6). */
.rv-zonebanner { position:relative; height:92px; margin:-6px 0 12px; border-radius:2px; overflow:hidden;
   background-size:cover; background-position:center; display:flex; align-items:flex-end;
   justify-content:center; padding-bottom:8px; }
.rv-zonename { font-family:var(--serif); color:#E6D5A8; font-size:14px; letter-spacing:.16em;
   text-transform:uppercase; text-shadow:0 1px 5px #000, 0 0 3px #000; }

/* FIX #13 (P2, owner playtest): tier unlocks are EVENTS — a celebratory banner on
   the results reveal itself when THIS hunt's outcome newly unlocked a tier
   (app.js's detectNewlyUnlockedTiers/buildRevealHtml). Violet ties it to the same
   progression accent .tierhint (Camp) uses, distinct from the gold .levelup chip
   (a character-level event) and the green win banner (this hunt's own outcome) —
   three different events, three different tones. rv-el/`.play` timing + the
   #reveal.done skip-to-resting-state rule are both inherited for free (generic
   `.rv-el` rules already cover any element with that class). */
.tierunlock { margin-top:8px; text-align:center; background:linear-gradient(180deg,#1A2029,#0F1319);
              border:1px solid #262C3666; color:#E6D5A8; border-radius:2px; padding:10px 12px;
              font-weight:800; font-size:13px; letter-spacing:.04em; }
.tierunlock .tulabel { display:block; font-size:10.5px; letter-spacing:.12em; color:#C3B48E;
                        margin-bottom:2px; font-weight:700; }
.tierunlock.play { animation:fadein .35s ease forwards, pulse 1.3s ease .15s 2; }

/* M1.6-D: the NEW BASE DISCOVERED reveal beat (web/app.js's newBaseDiscoveredHtml) —
   same shape/timing family as .tierunlock immediately above (celebratory rv-el banner,
   fades+pulses in alongside the first log line), but its own teal/emerald accent so a
   discovery event reads as visually distinct from a gear-gate event (.tierunlock,
   violet) and a character-level event (.levelup-card, gold). */
.newbase { margin-top:8px; text-align:center; background:linear-gradient(180deg,#12161C,#0D1013);
           border:1px solid #262C3666; color:#E6D5A8; border-radius:2px; padding:10px 12px;
           font-weight:800; font-size:13px; letter-spacing:.04em; }
.newbase .nblabel { display:block; font-size:10.5px; letter-spacing:.12em; color:#262C36;
                     margin-bottom:2px; font-weight:700; }
.newbase.play { animation:fadein .35s ease forwards, pulse 1.3s ease .15s 2; }

/* Hunt-loop depth §3 (docs/superpowers/specs/hunt-loop-depth.md): the reveal-shape
   flourish (web/app.js's revealShapeBeatHtml) — a one-line beat for the rare
   scrap_windfall/double_item/gold_purse drop shapes, landing at the SAME T.xp beat as
   the XP row/gold tick (scheduleRevealBody) right above the loot cards it's teasing.
   rv-el/`.play`/#reveal.done skip are all inherited for free, same reuse .omen-beat/
   .tierunlock above rely on. Rose accent keeps it visually distinct from every other
   reveal beat's own tone (omen amber, tierunlock violet, newbase teal, levelup gold). */
.revealshape-beat { text-align:center; margin-bottom:8px; }
.revealshape-beat.play { animation:fadein .35s ease forwards; }
.revealshapeline { color:#E6D5A8; font-size:13px; font-weight:700; letter-spacing:.02em; }

#reveal .log { margin-top:6px; }
#reveal .log.play { animation:fadein .3s ease forwards; }
#reveal .log b { color:#E6D5A8; }

/* M1.9 item 19 -- VOICES (DECISIONS D031). The one required visual contract: a SERIF
   register (the body font is 'Segoe UI' sans -- the voice deliberately reads as a
   different, older register). Two forms: `.worldvoice` (the world's own voice, no speaker,
   at uninhabited screens) and `.npcvoice` (a named inhabitant -- a portrait-placeholder
   frame + name + line). Both are plain in-flow blocks -- NEVER position:fixed, never an
   overlay, never over a button -- so they can never gate or intercept a transaction (the
   spec's non-blocking guarantee, enforced structurally in markup + asserted in tests). */
.voiceline { font-family:var(--serif); font-style:italic;
             color:#A2946C; font-size:13px; line-height:1.5; letter-spacing:.01em; }
.worldvoice { text-align:center; margin:14px auto 4px; max-width:34ch; opacity:.9; }
/* A tagged reactive line (the server keyed it off a recent event) glows a touch warmer so
   the "the NPC noticed what you just did" beat reads. */
.voiceline.reacting { color:#C3B48E; }

.npcvoice { display:flex; align-items:center; gap:10px; margin:10px 0 12px;
            padding:10px 12px; background:#0D0F13; border:1px solid #262C35;
            border-radius:2px; }
.npcportrait { flex:0 0 auto; width:40px; height:40px; border-radius:2px;
               background:linear-gradient(135deg,#262C36,#12151B);
               border:1px solid #2E343E; box-shadow:inset 0 0 12px #0009; }
/* Delivered NPC portrait: a transparent PNG sitting directly on the palette-matched .npcvoice
   panel — NO box/frame behind it (rule 5). Enlarged; a subtle radial floor grounds the figure
   without a rectangle. On 404 the img's onerror removes `has-art`, reverting to the box above. */
.npcportrait.has-art { width:60px; height:60px; background:transparent; border:0; box-shadow:none;
               border-radius:0; overflow:visible; }
.npcportrait.has-art img { width:100%; height:100%; object-fit:contain; object-position:center bottom;
               display:block; filter:drop-shadow(0 3px 8px #000000cc); }
/* Warden's Shade — the pale spectre on a palette panel (rule 3), transparent PNG with a faint
   NEUTRAL cool rim-light (never a rarity hue, rule 4). 404 -> .missing hides it (rule 6). */
.shade-intro { display:flex; align-items:center; gap:12px; background:#0D0F13; border:1px solid #262C35;
               border-radius:2px; padding:10px 12px; margin-bottom:10px; }
.shade-intro .arena-sparline { flex:1; margin:0; }
.shade-portrait { flex:0 0 auto; width:78px; height:78px; }
.shade-portrait img { width:100%; height:100%; object-fit:contain; object-position:center bottom;
               display:block; filter:drop-shadow(0 0 9px #c9d0dc33) drop-shadow(0 3px 8px #000000cc); }
.shade-portrait.missing { display:none; }
.npcvoicebody { min-width:0; }
.npcname { font-family:var(--serif); color:#C3B48E; font-size:12px;
           letter-spacing:.04em; margin-bottom:2px; }

.xpw { margin-top:10px; font-size:14px; }
.xpw.play { animation:fadein .3s ease forwards; }
.xptxt { color:#C3B48E; font-weight:700; }
.goldtxt { float:right; color:#E6D5A8; font-weight:700; }
.xpbar { clear:both; height:8px; background:#191D24; border-radius:4px; overflow:hidden; margin-top:6px; }
/* Ratified breach fix (5a): the XP / level bar is EMBER, not purple. Progression is not
   Epic; the level bar rides the sanctioned warm accent (energy-fill family). */
.xpfill { height:100%; width:0; background:linear-gradient(90deg,var(--bronze),var(--ember)); }
.xpfill.play { animation:xpgrow .6s ease-out forwards; }
/* T2.5 (QA-B5): the itemized level-up card (web/app.js's levelUpCardHtml) — replaces
   the old bare inline ".levelup" chip ("LEVEL UP -> N") with a full-width block:
   LEVEL {new} title, the +STR/+DEX/+VIT gains line, and an optional consequence
   line (nextGsGateLine's "N gear score to unlock <zone> T<tier>"). Keeps the SAME
   `id="rvLevelUp"` + rv-el convention the old chip used. Pre-rendered as rv-el
   (hidden), so it needs an explicit .play to animate in on the non-skip path; .done
   forces it visible on an early skip, same as every other rv-el. */
.levelup-card { display:block; clear:both; margin-top:8px; text-align:center;
                background:linear-gradient(180deg,#3a2410,#212730); border:1px solid #E6D5A866;
                color:#E6D5A8; border-radius:2px; padding:10px 12px; }
.levelup-card .lvup-title { font-size:14px; font-weight:800; letter-spacing:.08em; }
.levelup-card .lvup-gains { display:block; margin-top:4px; font-size:12.5px; font-weight:700;
                            color:#F2DDA6; }
.levelup-card .lvup-conseq { display:block; margin-top:6px; font-size:11px; font-weight:500;
                             color:#E6D5A8; opacity:.85; }
.levelup-card.play { animation:fadein .4s ease forwards, pulse 1.3s ease .15s 2; }

.lootlist { display:flex; flex-direction:column; }

/* Reveal beam (Iron Sanctum 4a / item 12): ONE radial rarity beam behind the drop card. The
   beam colour (per-rarity --beam-c) IS the announcement; radius (--beam-r) + bloom (--beam-o)
   scale with rarity, fed from app.js (revealBeamHtml) so the DOM data-attrs are the single
   source. Sits behind the cards (.rv-loot is the positioning context; cards get z-index:1). */
.rv-loot { position:relative; }
.rv-loot .dr { position:relative; z-index:1; }
.rv-beam { position:absolute; inset:-34px -10px; z-index:0; pointer-events:none;
           background:radial-gradient(circle at 50% 46%, var(--beam-c) 0%, transparent var(--beam-r,180px));
           opacity:var(--beam-o,.2); }
.rv-beam.r-uncommon  { --beam-c:#5AC67A; }
.rv-beam.r-rare      { --beam-c:#4A9EFF; }
.rv-beam.r-epic      { --beam-c:#A56BFF; }
.rv-beam.r-legendary { --beam-c:#E0B25A; }
.rv-beam.r-mythic    { --beam-c:#E5484D; }
/* Mythic's "tell": a second inset keyline framing the ceremony (spec 4a). */
.rv-beam.r-mythic::after { content:''; position:absolute; inset:20% 24%; border:1px solid #E5484D55; border-radius:3px; }

#reveal .dr { border-radius:2px; padding:9px 10px; background:#0E1014; border:1px solid #262C36;
              display:flex; gap:9px; align-items:center; margin-top:8px; }
#reveal .dr .ic2 { width:32px; height:32px; border-radius:2px; background:#191D24;
                    display:flex; align-items:center; justify-content:center;
                    font-size:17px; flex:none; }
#reveal .dr .nm2 { font-weight:700; }
#reveal .dr .pw, #reveal .dr .afx { color:#A99A70; font-size:11.5px; font-weight:400; }
#reveal .dr.dustcap { color:#E6D5A8; border-color:#3A4250; }

/* Loot cards reuse the r-<rarity> border/color classes (T18) — only the
   stagger/glow trigger is new here. Commons pop instantly-ish, no fanfare;
   legendary/mythic tease (below) before they burst.
   FIX #12 (P2): each uncommon+ rule now also layers a cardpulse/cardpulseBig
   border-pulse (scale bump) into the SAME animation shorthand — a separate class
   selector can't be layered on top since only one `animation` declaration ever wins
   per element, so the pulse has to live in this same comma-separated list. Iteration
   count/duration climbs with rarity ("Uncommon noticeable -> Mythic overwhelming"). */
#reveal .dr.play.r-common { animation:popin .18s ease-out forwards; }
#reveal .dr.play.r-uncommon { animation:popin .25s ease-out forwards, flashg .55s ease, cardpulse .5s ease-out 1; }
#reveal .dr.play.r-rare { animation:flipin .5s cubic-bezier(.2,1.4,.4,1) forwards, glowb 1.3s ease, cardpulse .55s ease-out 2; }
#reveal .dr.play.r-epic { animation:flipin .5s cubic-bezier(.2,1.4,.4,1) forwards, glowp 1.4s ease, cardpulse .6s ease-out 2; }
#reveal .dr.play.r-legendary { animation:flipin .55s cubic-bezier(.2,1.4,.4,1) forwards, glowo 1.9s ease, cardpulseBig .7s ease-out 2; }
#reveal .dr.play.r-mythic { animation:flipin .6s cubic-bezier(.2,1.4,.4,1) forwards, glowr 2.4s ease, cardpulseBig .85s ease-out 3; }
#reveal .dr.tease-legendary { animation:tease .5s ease; }
#reveal .dr.tease-mythic { animation:tease .8s ease; }
/* FIX #30: the hunt-3 scripted-rare's UNIQUE burst — replaces the standard Rare's
   flipin+glowb with a bigger overshoot pop + the gold glowo glow (not blue), so it's
   audibly AND visibly distinct from an ordinary Rare drop. 4-class selector
   (.dr.play.ceremony-burst.r-rare) so its specificity beats .dr.play.r-rare
   unconditionally, regardless of source order. */
#reveal .dr.play.ceremony-burst.r-rare { animation:ceremonyburst .8s cubic-bezier(.2,1.6,.4,1) forwards, glowo 2s ease, cardpulseBig .9s ease-out 2; }

.nudge { display:block; margin-top:12px; text-align:center; color:#E6D5A8; font-size:12.5px; }
.nudge.play { animation:fadein .4s ease forwards; }

.again.play { animation:fadein .4s ease forwards, pulse 1.6s ease .4s infinite; }

/* ---- T2.4: Results crossroads — up to 2 contextual "what next" actions rendered
   directly under HUNT AGAIN (app.js's buildCrossroadsActions), data-driven from the
   hunt payload: a Rare+ drop -> Forge, a level-up -> Character, a tier unlock -> the
   newly-opened tier. Lands at the SAME buttonTime as HUNT AGAIN itself (scheduleReveal),
   styled as a visually subordinate secondary row (bordered, not the red .bigbtn) so
   HUNT AGAIN stays the obvious primary action. */
.crossroads { display:flex; flex-direction:column; gap:8px; margin-top:10px; }
.crossroads.play { animation:fadein .4s ease forwards; }
.crossbtn { width:100%; background:#181C24; border:1px solid #2E343E; border-radius:2px;
            color:#C3B48E; padding:11px 0; font-weight:700; font-size:13px;
            letter-spacing:.03em; text-align:center; }
.crossbtn:active { background:#191D24; }

/* ---- Task 3.E: M1.6-E encounter interrupts — the "press the attack or withdraw"
   hook (app.js's encounterHookHtml), landing at the same buttonTime as HUNT AGAIN.
   PRESS THE ATTACK reuses .bigbtn (a genuine escalation, same visual weight as HUNT
   AGAIN); Withdraw is a plain bordered-quiet secondary, mirroring .crossbtn's
   "visually subordinate" treatment. Baseline layout only — a distinct juice/sting
   treatment is a Wave 4 visual-polish item, not built here. */
.encounter-hook { margin-top:12px; background:#0F1319; border:1px solid #2E343E;
                  border-radius:2px; padding:12px; text-align:center; }
.encounter-hook.play { animation:fadein .4s ease forwards; }
.enclabel { color:#C9B98E; font-size:13px; font-style:italic; }
/* DECISIONS D012 (escalation ruling #2): a LOST accept renders distinctly from a
   WIN — the same red accent .loss already uses for the main hunt DEFEAT banner
   (compound selector so it reliably wins over .enclabel's own color declaration
   regardless of stylesheet order). small (the "(consolation)" aside) is muted the
   same way the hunt defeat card's own consolation chip is (styles below, FIX #7/
   #29 defeat content pass). */
.enclabel.loss { color:#C4553E; }
.enclabel.loss small { color:#A99A70; font-style:normal; }
.encbtns { display:flex; flex-direction:column; gap:8px; margin-top:10px; }
.encaccept { background:linear-gradient(180deg,#171B22,#0E1014); color:var(--ember); box-shadow:var(--ember-glow); }
.encdecline { width:100%; background:transparent; border:1px solid #262C36; border-radius:2px;
              color:#A99A70; padding:10px 0; font-weight:600; font-size:12.5px; }
.encdecline:active { background:#191D24; }

/* Hunt-loop depth §1 (HUNT CHAINS): the Delve deeper/Bank spoils choice, both inside
   the results reveal (.chain-hook, id="rvChain" — mirrors .encounter-hook's own
   prompt+2-button shape exactly, same "a choice after a win" visual weight) and on
   the Camp screen's mid-chain restore panel (.chainrestore — mirrors .minepot's own
   "at risk" gold-accent readout, reusing .frcard for its own outer card). DELVE
   DEEPER reuses .bigbtn (a genuine escalation, same weight as HUNT AGAIN/PRESS THE
   ATTACK); BANK SPOILS is a plain bordered-quiet secondary, mirroring .encdecline. */
.chain-hook { margin-top:12px; background:#181C24; border:1px solid #4a3a10;
             border-radius:2px; padding:12px; text-align:center; }
.chain-hook.play { animation:fadein .4s ease forwards; }
.chainrestore { text-align:center; margin-top:10px; }
.chainlabel { color:#E6D5A8; font-size:13px; font-weight:600; }
.chainlabel.chainlost { color:#C4553E; }
.chainbtns { display:flex; flex-direction:column; gap:8px; margin-top:10px; }
.chaindelve { background:linear-gradient(180deg,#1B1F26,#12151B); color:var(--ember); box-shadow:var(--ember-glow); }
.chainbank { width:100%; background:transparent; border:1px solid #262C36; border-radius:2px;
            color:#A99A70; padding:10px 0; font-weight:600; font-size:12.5px; }
.chainbank:active { background:#191D24; }
.chainmaxed { color:#A99A70; font-size:12px; font-style:italic; margin-bottom:2px; }
/* M1.9 "THE READ" — WITHDRAW (D024/D026): the OCCASIONAL third option ("keep the pot,
   lose your nerve"). Styled between the bold DELVE and the plain BANK — a muted amber
   "caution" tone (not the loud gold of DELVE, not the flat outline of BANK) with the
   server-authored cost sub-line. .chainwithdrew is the terminal cut-your-losses line. */
.chainwithdraw { width:100%; background:transparent; border:1px dashed #6b5a2e; border-radius:2px;
                 color:#C3B48E; padding:9px 8px; font-weight:700; font-size:12.5px;
                 display:flex; flex-direction:column; gap:2px; align-items:center; cursor:pointer; }
.chainwithdraw:active { background:#212730; }
.withdrawcost { color:#9a8b60; font-size:11px; font-weight:500; font-style:italic; }
.chainlabel.chainwithdrew { color:#C3B48E; }
/* Phase 1.1 (name the pot): each pot item's name rides the shared .r-<rarity> color
   (styles.css line ~312), so .potitem only adds the weight that makes a named Rare read
   as loot, not prose. Phase 1.3 (wipes sting): .chainforfeit is the itemized-loss line;
   .chainwipe-flash is the one-shot red pulse behind the wiped label (playDefeatThud is the
   audio half) — a wipe should register in the body, not just the text. */
.potitem { font-weight:700; }
.chainforfeit { color:#B79A88; font-weight:500; font-size:12px; }
.chainwipe-flash { animation:chainwipeflash .6s ease-out 1; border-radius:2px; }
@keyframes chainwipeflash {
  0% { background:#3a0f0f; box-shadow:0 0 20px #C4553E99; }
  100% { background:transparent; box-shadow:0 0 0 transparent; }
}

/* ---- M1.9 A2 (results CTA hierarchy: at most one PRIMARY + labeled secondaries +
   an always-present exit) --------------------------------------------------------
   When an escalation offer (chain/encounter) owns the PRIMARY slot, HUNT AGAIN
   demotes to .again-secondary — a plain bordered-quiet action (mirrors .encdecline/
   .chainbank/.crossbtn's subordinate treatment) so the offer's own accept is the lone
   loud button. .rvforfeit is the explicit forfeit copy above it (hunting again ENDS
   the offer). .rvexit is the standing "← Camp" dismiss — always present so a player is
   never trapped behind the full-screen reveal (matters most when HUNT AGAIN is
   disabled for want of energy). .chain-demoted tones a chain card's DELVE down to
   secondary weight when an encounter co-occupies the reveal (one loud primary only). */
.again-secondary { width:100%; background:#181C24; border:1px solid #2E343E;
                   border-radius:2px; color:#C3B48E; padding:11px 0; font-weight:700;
                   font-size:13px; letter-spacing:.03em; text-align:center; }
.again-secondary:active { background:#191D24; }
.again-secondary[disabled], .bigbtn.again[disabled] { opacity:.5; }
.rvforfeit { color:#A99A70; font-size:12px; font-style:italic; margin-top:12px; margin-bottom:2px; }
/* EXIT item 2: the walled status line — a quiet fact where the dead button stood. */
.rvnexthunt { text-align:center; font-size:12px; color:#6A6E77; font-style:italic; margin-top:8px; }
.rvexit { display:block; margin:14px auto 0; background:transparent; border:none;
          color:#83764F; font-size:12.5px; font-weight:600; letter-spacing:.02em;
          padding:8px 14px; cursor:pointer; }
.rvexit:active { color:#C3B48E; }
.chain-demoted .chaindelve { background:#181C24; border:1px solid #2E343E;
                             box-shadow:none; color:#C3B48E; font-size:13px; }

/* M1.9 "THE READ" item 9 (per-depth danger line): the NEXT depth's server-computed
   threat band (reusing .threatchip/.threat-<band> above) + rarity-floor indicator,
   sitting under the "at risk" pot label as the truthful read before a delve. */
.chaindanger { color:#A99A70; font-size:12px; margin:4px 0 2px; }
.chainrarityup { color:#C3B48E; font-weight:600; }
/* M1.9 item 12 (each delve is a fight): the combat beat — a depth tag + enemy name +
   combat line. The intensity tier (near/mid/deep) escalates the accent so a depth-3
   fight reads visibly deeper than a depth-1 one (alongside the rising-pitch audio). */
.chaincombat { font-size:12.5px; margin:2px 0 6px; padding:5px 8px; border-radius:2px;
               border-left:3px solid #262C36; background:#13161C; }
.chaincombat-near { border-left-color:#E6D5A866; }
.chaincombat-mid  { border-left-color:#E6D5A888; }
.chaincombat-deep { border-left-color:#C4553Eaa; background:#1c1620; }
.chaindepthtag { font-weight:700; color:#E6D5A8; }
.chainenemy { font-weight:600; color:#E6D5A8; }
.chaincombatline { color:#A99A70; }
/* Enemy portraits (2026-07-16 drop): near-black-vignetted square busts (ENEMY_PORTRAIT,
   app.js), shown on the delve combat beat + the elite announcement. Display contract is
   ~96-128px (wiring ticket) — pinned as a COMPUTED check in tests/test_css_computed.py,
   never as source text (CLAUDE.md rule 5). Iron-frame border, no reserved rarity hues. */
.enemyportrait { display:block; width:96px; height:96px; border-radius:2px;
                 border:1px solid #2E343E; background:#0B0D11; }
.chaincombat-withportrait { display:flex; align-items:center; gap:10px; }
.chaincombat-withportrait .enemyportrait { flex:0 0 auto; }
.chaincombat-withportrait .chaincombatbody { min-width:0; }
/* The announcement gets the larger cut (still inside the 96-128 contract), centered
   above the prompt — the elite should read like a reveal, not a list row. */
.encportraitwrap { display:flex; justify-content:center; margin-bottom:8px; }
.encportraitwrap .enemyportrait { width:112px; height:112px; }

/* ---- FIX #7/#29 (P2, owner playtest): defeat content pass ------------------------
   Defeat used to be just a banner + one hardcoded line. The consolation-XP row reuses
   the win path's own .xpw/.xptxt/.xpbar/.xpfill/.levelup-card classes verbatim (all
   already rarity/won-agnostic) — only the enemyhp- and defeatscrap- rules below are new. enemyhpfill
   is a cosmetic-only "so close" bar (app.js's defeatHpPct() computes NOTHING that
   affects any real outcome — see its own comment) that DRAINS from a full bar down to
   its near-empty --hp-to target, dramatizing "the enemy nearly ran out of HP". */
.enemyhpwrap { margin-top:10px; font-size:12px; }
.enemyhpwrap.play { animation:fadein .3s ease forwards; }
.enemyhplabel { color:#83764F; letter-spacing:.05em; }
.enemyhplabel .soclose { color:#C4553E; font-style:italic; margin-left:6px; }
.enemyhpbar { height:8px; background:#191D24; border-radius:4px; overflow:hidden; margin-top:6px; }
.enemyhpfill { height:100%; width:100%; background:linear-gradient(90deg,#C4553E,#A83A2A); }
.enemyhpfill.play { animation:hpdrain .65s ease-out forwards; }
@keyframes hpdrain { from{width:100%} to{width:var(--hp-to,15%)} }
#reveal.done .enemyhpfill { width:var(--hp-to,15%) !important; }
/* .defeatscrap reuses the existing #reveal .dr/.r-common chrome + popin (no fanfare,
   same as any other common loot card) — just its own text color/size for the
   "(consolation)" qualifier. */
.defeatscrap { color:#E6D5A8; }
.defeatscrap small { color:#83764F; font-size:10px; margin-left:4px; }

/* ---- Character Screen: Wanderer Paper-Doll (T22) ----------------------------------
   .dollwrap (renamed from the mockup's .dollwrap3) / .figsvg / .slotf and its s-*
   rarity-tint variants are lifted VERBATIM (positions, sizes, colors, box-shadows)
   from docs/mockups/doll-v3-and-reveal.html's approved v3 figure (owner verdict "1A
   wanderer: approved" — "ship this figure + ten ornamented anchors as the M1
   character screen"), including the exact per-anchor top/left pixel offsets applied
   inline in app.js's DOLL_ANCHORS table. s-common/s-epic/s-legendary/s-mythic are
   ADDED here (the mockup only ever demoed s-unc/s-rare, its two equipped example
   slots) — same inset-shadow + glow formula the mockup established, extended to the
   other 4 rarities using the exact colors already canonical in this file's own
   .r-<rarity> classes / T21's glow keyframes (not invented). */
/* The arena PROFILE paper-doll keeps the original absolute layout (.dollwrap positioning
   context + .slotf placed by inline top/left). Base .slotf is the absolute (profile) form;
   the Character reframe below overrides it inside .socketcol. */
.dollwrap { position:relative; width:320px; height:342px; margin:0 auto; }
.dollwrap .figsvg { position:absolute; left:50%; top:6px; transform:translateX(-50%); height:330px; }
.slotf { position:absolute; width:66px; box-sizing:border-box; background:linear-gradient(180deg,#0E1014f2,#090A0Df2); border:1px solid var(--iron); border-radius:2px; padding:6px 4px 4px; text-align:center; font-size:8px; letter-spacing:.05em; color:#A2946C; box-shadow:inset 0 2px 6px rgba(0,0,0,.85); cursor:default; }
.slotf::before { content:'◆'; position:absolute; top:-7.5px; left:50%; transform:translateX(-50%); font-size:8px; text-shadow:0 0 6px currentColor; }

/* Iron Sanctum character reframe (design pass item 11): the doll, the two flanking socket
   columns and the stat grid resolve into ONE bracketed framed object (.charpanel). The
   sockets flank the figure in real grid columns with a gutter — no card ever overlaps the
   figure — and the stat grid is inset into the same panel. Holds at desktop and 390px. */
.charpanel { position:relative; background:var(--panel); border:1px solid var(--iron);
             border-radius:2px; padding:16px 14px 14px; margin-bottom:14px; }
/* 2px bronze corner brackets (5a: primary panels only), two opposite corners */
.charpanel::before, .charpanel::after { content:''; position:absolute; width:13px; height:13px; pointer-events:none; }
.charpanel::before { top:-1px; left:-1px; border-top:2px solid var(--bronze); border-left:2px solid var(--bronze); }
.charpanel::after  { bottom:-1px; right:-1px; border-bottom:2px solid var(--bronze); border-right:2px solid var(--bronze); }
.dollgrid { display:grid; grid-template-columns:minmax(62px,76px) 1fr minmax(62px,76px); gap:11px; align-items:stretch; }
.socketcol { display:flex; flex-direction:column; gap:9px; }
.figcol { position:relative; display:flex; align-items:center; justify-content:center; min-height:300px; }
.figcol .figsvg { position:static; transform:none; max-width:100%; max-height:322px; height:auto;
                  object-fit:contain; filter:drop-shadow(0 6px 18px #000000cc); }
/* Equipment socket in the reframe — a recessed well (5a: 56px, inset shadow), a grid item
   (not absolutely placed). Overrides the base .slotf above inside the flanking columns. */
.socketcol .slotf { position:relative; width:auto; flex:1 1 0; min-height:54px; padding:5px 4px;
         display:flex; flex-direction:column; justify-content:center; overflow:hidden; }
.socketcol .slotf::before { content:none; }
.slotf b { display:block; font-size:8.5px; letter-spacing:.14em; }
.slotf small { display:block; color:#6A6E77; font-size:7.5px; margin-top:1px; }
.slotf.tappable { cursor:pointer; }
.s-common { border-color:#9C927A; color:#9C927A; box-shadow:inset 0 0 10px #000000aa, 0 0 12px #9C927A30; }
.s-unc  { border-color:#5AC67A; color:#5AC67A; box-shadow:inset 0 0 10px #000000aa, 0 0 12px #5AC67A30; }
.s-rare { border-color:#4A9EFF; color:#4A9EFF; box-shadow:inset 0 0 10px #000000aa, 0 0 12px #4A9EFF30; }
.s-epic { border-color:#A56BFF; color:#A56BFF; box-shadow:inset 0 0 10px #000000aa, 0 0 12px #A56BFF30; }
.s-legendary { border-color:#E0B25A; color:#E0B25A; box-shadow:inset 0 0 10px #000000aa, 0 0 12px #E0B25A30; }
.s-mythic { border-color:#E5484D; color:#E5484D; box-shadow:inset 0 0 10px #000000aa, 0 0 12px #E5484D30; }
.s-common small, .s-unc small, .s-rare small, .s-epic small, .s-legendary small, .s-mythic small { color:#A99A70; }
.s-empty { border-style:dashed; color:#6A6E77; }
/* Item 4(b): the empty socket's ghost — the slot's own glyph, dimmed to a suggestion.
   With the dashed outline this reads EMPTY at a glance; the M2-locked socket below
   stays solid near-black + 🔒 (no ghost) so the two can never be confused. */
.slotghost { display:block; font-size:13px; line-height:1; opacity:.35;
             filter:grayscale(1); margin:2px 0 1px; }
/* WAR VOICE item 1: the war-status banner (Camp + Map) — iron panel, bronze rule,
   never a takeover. The Map claim-card LIVE badges ride the same accent. */
.warstatus { border:1px solid #262C36; border-left:3px solid #8A6A2E; border-radius:2px;
             background:#0E1014; padding:10px 12px; margin-bottom:12px; }
.warstatus-head { font-size:13.5px; color:#E6D5A8; letter-spacing:.03em; margin-bottom:4px; }
.warstatus .btn2 { margin-top:8px; }
.clivebadge { font-size:8.5px; letter-spacing:.14em; color:#E7A83E; border:1px solid #8A6A2E;
              border-radius:2px; padding:1px 5px; margin-left:8px; vertical-align:2px;
              white-space:nowrap; }
.clivebadge.muster { background:#171B22; }
/* WAR VOICE item 2: the muster's teaching layer. */
.planteach { font-size:11.5px; color:#A99A70; }
.planteach b { color:#C3B48E; letter-spacing:.06em; }
.hpswitchbar { height:6px; background:#14181f; border:1px solid #262C36; border-radius:2px;
               overflow:hidden; }
.hpswitchbar i { display:block; height:100%; background:#8A6A2E; }
.warshort { font-size:12px; color:#C4553E; margin-top:6px; }  /* the loss accent, never Mythic red */
/* THE HOUSE DOOR (war-QA P1): the Map's assignment doorway + the one confirm
   ceremony. House color rides --hc (server data), never a reserved rarity hue. */
.pledgedoor { border:1px solid #262C36; border-left:3px solid #8A6A2E; border-radius:2px;
              background:#0E1014; padding:12px 14px; margin:12px 0; }
.pledgecard { text-align:center; }
.pledgesigil { font-size:44px; margin:10px 0 2px; color:var(--hc, #C3B48E); }
.pledgename { font-family:var(--serif); font-size:20px; letter-spacing:.08em;
              color:var(--hc, #E6D5A8); margin-bottom:6px; }
/* Stale-process hygiene (owner 2026-07-17): the codex footer names the RUNNING
   server's build — a human diagnoses a stale process in seconds ('?' -> bottom line
   vs `git rev-parse --short HEAD`). */
.codexbuild { margin-top:14px; padding-top:8px; border-top:1px solid #191D24;
              font-size:9px; letter-spacing:.14em; color:#3E444E; text-align:center; }
/* Item 2 (geared doll): overlays stack over the figure; the glint is a soft rarity
   EDGE from the reserved s-* classes (their border-color + glow — never a new hue).
   An empty overlay table renders the stack as just the figure, byte-identical. */
.dollstack { position:relative; display:inline-block; }
.dollstack .dolloverlay { position:absolute; inset:0; width:100%; height:100%;
                          object-fit:contain; pointer-events:none; }
.dollglint { position:absolute; inset:0; pointer-events:none; border-radius:4px;
             border:1px solid; opacity:.35; }
.s-lock { border-color:#191D24; color:#2E343E; background:linear-gradient(180deg,#0b101cf5,#080b14f5); }
.s-lock small { color:#262C35; }

.charname { text-align:center; margin-top:10px; font-size:18px; font-weight:800; color:#E6D5A8; cursor:pointer; }
.charname .pencil { color:#6A6E77; font-size:13px; margin-left:5px; }
.stats { display:grid; grid-template-columns:repeat(4,1fr); gap:6px; text-align:center; margin-top:12px; }
.stats div { background:#0E1014; border:1px solid #262C36; border-radius:2px; padding:7px 0; font-size:11px; color:#83764F; }
.stats b { display:block; color:#E6D5A8; font-size:15px; margin-top:2px; }
.gearscore { text-align:center; color:#E7A83E; margin:10px 0 16px; font-size:13px; letter-spacing:.05em; }
.gearscore b { font-size:16px; }
.inv { display:grid; grid-template-columns:repeat(6,1fr); gap:6px; }
/* Neither border-color NOR color is set here, and that's deliberate, not an
   oversight -- either one, even as its own longhand, would still compete with
   .r-<rarity> (T18) for the same property, and since this rule sits after
   .r-<rarity> in the file, an equal-specificity fight would go to source order and
   .cell would always win, silently showing every rarity with the same neutral
   border (confirmed live before this fix: an uncommon .cell's border-color stayed
   #262C36 instead of green -- relocating border-color into its own longhand, tried
   first, did NOT fix it, since it was still a competing declaration for the same
   property). Leaving both fully undeclared means .r-<rarity> applies with zero
   competition, correct regardless of file order; the one case with no rarity class
   (the empty-inventory placeholder) just inherits the ambient neutral text color,
   which is exactly the "no special rarity" look that case wants anyway. */
.cell { position:relative; aspect-ratio:1; background:#0E1014; border-width:1px; border-style:solid;
        border-radius:2px;
        display:flex; align-items:center; justify-content:center; font-size:16px; cursor:pointer; }
.cell.empty { cursor:default; opacity:.35; }
/* Item 4(c): the QUICK EQUIP — hidden until the tile is hovered/focused, and only on
   devices that HAVE hover (touch keeps tap→sheet; the sheet's EQUIP is the mobile
   path). Iron chrome, parchment text — no reserved rarity hues on an action. */
.equip-quick { display:none; position:absolute; left:50%; bottom:2px; transform:translateX(-50%);
  font-size:8px; letter-spacing:.08em; font-weight:700; padding:2px 6px;
  background:#171B22f0; color:#E6D5A8; border:1px solid #2E343E; border-radius:2px;
  cursor:pointer; z-index:2; }
@media (hover: hover) {
  .cell:hover .equip-quick, .cell:focus-within .equip-quick { display:block; }
}
/* T2.6 (level-gate disclosure): 🔒Lv badge on an inventory tile whose base level_req
   exceeds the character's current level — display only (app.js's renderCharacter
   compares two server-provided numbers; the server still enforces LevelTooLow on
   equip). Dimmed tile + a small bottom-right badge, same "still tappable, visibly
   not-yet-wearable" treatment as the item sheet's disabled EQUIP button below. */
/* POST-PASS-7 #15 (owner): a tile the character CANNOT equip yet (level-locked) reads
   loud — red border + tint over the rarity color (border-color !important deliberately
   out-ranks the r-<rarity> tint: "can't wear it" outranks "how good it is" on the
   tile; the sheet still shows the full rarity read). The 🔒Lv badge stays. */
.cell.locked { opacity:.75; border-color:#C4553E !important;
               box-shadow: inset 0 0 0 1px #C4553E55; background:#1a1216; }
.cell.locked .lvbadge { color:#C4553E; }
.lvbadge { position:absolute; right:2px; bottom:2px; background:#000000cc; color:#C4553E;
           font-size:7.5px; font-weight:700; letter-spacing:.02em; padding:1px 3px;
           border-radius:4px; line-height:1.3; }
/* #3 (P2, cert Day-1 batch T2): a small ▲ upgrade pip (app.js's renderCharacter) on
   any inventory tile whose item.power beats whatever's equipped in its slot — same
   dark-badge language as .lvbadge just above, opposite corner (top-right, so a
   simultaneously locked+upgrade tile shows both badges without overlapping) and the
   SAME green the compare panel's own ▲ upgrades use (.up above) rather than a new hue. */
.pip-upgrade { position:absolute; right:2px; top:2px; background:#000000cc; color:#E6D5A8;
               font-size:9px; font-weight:800; line-height:1; padding:1px 3px; border-radius:4px; }

/* ---- Item Sheet + Equip-Compare (T22) ----------------------------------------------
   Classes/colors/gradients LIFTED from docs/mockups/item-detail.html's approved v4
   design (owner verdict "A — approved, design is complete"), sized up from that
   file's 264px thumbnail preview to real mobile density — same "direction, not
   literal thumbnail pixels" adaptation T20/T21 already applied to the other three
   screens' mockups. Colors/gradients/border-radii proportions unchanged. */
.modal-backdrop { position:fixed; inset:0; z-index:90; background:#000000b3;
                   display:flex; align-items:center; justify-content:center;
                   padding:20px 14px; overflow-y:auto; }
/* M1.9 P0 tooltip fix (Option A, D035): the item sheet opened FROM the results reveal
   (a committed drop card's tap-to-sheet) must stack ABOVE #reveal (z-index:100) — the
   default backdrop's 90 would put the sheet UNDER the results screen. 180 sits above
   #reveal/#screenflash(150)/.rv-takeover(160) but below #toast(200), so errors from the
   sheet's own actions still read over it. Scoped: no other modal's stacking changes. */
.modal-backdrop.over-reveal { z-index:180; }
/* The committed drop card is a tappable doorway (tap-to-sheet); a provisional chain-pot
   preview card never gets .tappable (no action on a ghost — hover reads its stake). */
.dr.tappable { cursor:pointer; }
/* D037 posted-odds: the delve danger line's numeric defeat % — the wager's posted wheel.
   Amber-red, heavier than the surrounding copy, matching the at-risk chainlabel tone. */
.chaindanger .chainpct { color:#C4553E; font-weight:700; }
/* D043 music bed: the quiet ♪ volume corner at the bottom of Camp — deliberately muted
   chrome (settings, not content); the range track inherits the accent. */
.musicrow { display:flex; align-items:center; gap:8px; justify-content:center;
            margin-top:14px; opacity:.55; }
.musicrow:hover { opacity:.9; }
.musicrow .musicico { color:#83764F; font-size:13px; }
.musicrow input[type="range"] { width:110px; accent-color:#262C36; }
.modal-card { width:100%; max-width:380px; position:relative; }
.modal-close { position:absolute; top:-34px; right:0; width:30px; height:30px;
               background:#191D24; border:1px solid #353C46; border-radius:2px;
               color:#C3B48E; font-size:15px; line-height:1; }

.isheet { background:#0D0F13; border:1px solid #E6D5A8; border-radius:3px; padding:14px;
          box-shadow:0 0 20px #E6D5A822, inset 0 0 14px #00000088; color:#C3B48E;
          font-family:var(--sans); }
/* No `color` of their own — DELIBERATE, not an oversight. The mockup's own
   #E0B25A/#E0B25A were specific to its single Rare demo item, not a "default for
   every item" choice, and a same-specificity same-property fight against the
   applied `.r-<rarity>` class (both single-class selectors) would be decided by
   source order, not by which one is "supposed" to win — confirmed live (an
   uncommon item rendered #9C927A gray instead of #E0B25A green before this was
   fixed). Leaving `color` unset here means .r-uncommon/.r-rare/.r-epic/
   .r-legendary/.r-mythic (T18) always win outright (no competing rule to out-order),
   and Common — the one rarity whose .r-common (T18) deliberately sets no color,
   see styles.css's rarity classes above — falls through to .isheet's own neutral
   #C3B48E, which *is* the correct "no special rarity color" treatment. */
.iname { font-weight:800; font-size:17px; }
.itag { font-size:11px; letter-spacing:.14em; margin-top:2px; }
.imeta { color:#6A6E77; font-size:12px; margin-top:2px; }
.imeta.origin { margin-top:10px; }
.ipower { float:right; text-align:right; color:#E6D5A8; font-weight:700; font-size:15px; }
.ipower small { color:#6A6E77; font-weight:400; font-size:9px; }
.implicit { margin-top:10px; padding:8px 10px; background:#0A0B0E; border-radius:2px;
            color:#B2A379; font-size:13px; border-left:2px solid #353C46; clear:both; }
.implicit b { color:#E6D5A8; }
.implicit .from { float:right; color:#6A6E77; font-size:11px; }
.affrow { margin-top:8px; padding:8px 10px 9px; background:#0A0B0E; border-radius:2px; }
/* Owner display law: affix VALUES are always neutral #E6D5A8 — the qbar below carries
   the goodness signal. Never colored red/green here. */
.affrow .v { color:#E6D5A8; font-weight:700; font-size:14px; }
.affrow .t { float:right; color:#83764F; font-size:11px; }
.affrow .t b { color:#C3B48E; }
/* TASK 47 (P2): utility affixes (xp_gain/loot_find) contribute 0 to gear score —
   dimmed row + a small "UTILITY · +0 GS" tag, distinct from the owner display law's
   red/green rule above (this is informational, not a goodness signal, so it stays
   neutral-toned like .neu rather than red/amber). */
.affrow.utility { opacity:.78; }
.utag { display:block; clear:both; color:#6A6E77; font-size:10px; letter-spacing:.04em;
        margin-top:2px; }
.forgerow .t .utag { display:inline; margin:0; }
.qbar { height:5px; background:#191D24; border-radius:3px; margin-top:6px; position:relative; overflow:hidden; clear:both; }
.qbar>div { height:100%; border-radius:3px; background:linear-gradient(90deg,#E6D5A8,#E6D5A8); }
.qlab { font-size:10px; color:#6A6E77; margin-top:3px; display:flex; justify-content:space-between; }
.afflines-note { margin-top:8px; color:#6A6E77; font-size:11px; text-align:center; }

/* Behavior-changing affixes (cert Day-2/Cowork #6, hunt-loop-depth.md §2): NOT
   stat lines -- a distinct left accent (the same violet .affrow .t b already
   uses for tier chrome, so the row still reads as "part of this item's affix
   family") plus a "PLAYSTYLE · +0 GS" tag (the same shape/tone as .utag above,
   never a goodness color per the owner display law) and an italic effect
   sentence (behaviorAffixEffectText's interpolated description) instead of a
   raw "+value% STAT" primary line. */
.affrow.behavior { border-left:2px solid #C3B48E; }
.affrow.behavior .v.behavior-label { color:#C9B98E; }
.behavior-desc { clear:both; color:#B2A379; font-size:12px; font-style:italic; margin-top:4px; }
.forgerow.behavior .behavior-desc { font-size:11px; margin-top:3px; }

/* #3 (P2, cert Day-1 batch T2): the item sheet's own ▲/▼ delta chips (app.js's
   itemSheetDeltaChipsHtml), shown BEFORE the equip press. Each chip is one of
   deltaRowHtml's OWN `<div><span>label</span><span class="up|dn|neu">...</span></div>`
   rows (showCompare's row renderer, reused verbatim, unmodified) — this box just lays
   two of them out side by side as compact chips instead of the compare panel's own
   stacked full-width rows (.delta/.delta div above); .up/.dn/.neu are the SAME global
   color classes the compare panel's rows already use, so the ▲ green/▼ red/gray-
   neutral language can never visually disagree between the two surfaces. */
.isheet-deltachips { display:flex; gap:8px; margin-top:10px; }
.isheet-deltachips > div { flex:1; background:#0A0B0E; border-radius:2px; padding:6px 9px;
                            font-size:12px; display:flex; justify-content:space-between; gap:6px; }
.btns { display:flex; gap:7px; margin-top:12px; }
.btns div { flex:1; text-align:center; border-radius:2px; padding:10px 0; font-weight:700; font-size:12px; }
.b-eq { background:linear-gradient(180deg,#8A6A2E,#8A6A2E); color:#06251c; cursor:pointer; }
.b-rr { background:linear-gradient(180deg,#171B22,#0E1014); color:var(--ember); cursor:pointer; }
.b-sl { background:#191D24; border:1px solid #353C46; color:#C3B48E; cursor:pointer; }
.btns .dis { opacity:.4; cursor:default; }
/* M1.9 item 23 (NAMED ITEMS, D034): the item-sheet NAME/RENAME control — its own row below
   the primary EQUIP/REROLL/SELL row (a secondary, cosmetic gold-sink action, not one of the
   three core verbs). A warm gold cast keys it to the currency it spends. The .dis variant
   (unaffordable) dims like every other affordability-gated control. */
.b-name { margin-top:8px; text-align:center; border-radius:2px; padding:9px 0; font-weight:700;
          font-size:12px; background:#212730; border:1px solid #6b551f; color:#E6D5A8; cursor:pointer; }
.b-name.dis { opacity:.45; cursor:default; }
/* M1.9 item 23: a player-given custom item name (itemNameHtml). Reads as the headline
   everywhere an item name renders, keyed warm-gold to the gold sink that bought it and
   lightly italic so it never masquerades as the item's real base name. */
.customname { font-style:italic; color:#C3B48E; }
/* M1.9 item 23: the LIFETIME STATS page (buildLifetimeStatsHtml) — a read-only key/value
   ledger of server-computed lifetime numbers, reachable from a profile. Each .ltrow carries
   its own tooltip (item-14 tip()). */
.lifetime-open { width:100%; margin-top:8px; }
.lifetime-grid { margin-top:10px; display:flex; flex-direction:column; gap:2px; }
.ltrow { display:flex; justify-content:space-between; align-items:baseline; gap:12px;
         padding:8px 10px; border-radius:2px; background:#0D0F13; }
.ltrow:nth-child(odd) { background:#0A0B0E; }
.lt-k { color:#998B62; font-size:12px; }
.lt-v { color:#E6D5A8; font-weight:700; font-size:13px; text-align:right; }
/* M1.9 item 23: the REFERRAL / INVITE hub (buildReferralHtml). */
.referral-open { width:100%; margin-top:8px; }
.referral-sub { color:#A99A70; font-size:12px; margin:6px 0 10px; }
.referral-code { display:flex; justify-content:space-between; align-items:center; gap:10px;
                 padding:10px 12px; border-radius:2px; background:#0A0B0E; border:1px solid #2E343E; }
.rc-label { color:#83764F; font-size:11px; letter-spacing:.06em; }
.rc-value { color:#C3B48E; font-weight:800; font-size:18px; letter-spacing:.12em; }
.referral-link { margin-top:6px; padding:8px 10px; border-radius:2px; background:#0D0F13;
                 color:#C3B48E; font-size:11px; word-break:break-all; cursor:pointer; }
.referral-stats { display:flex; gap:16px; margin:10px 0; font-size:12px; color:#A99A70; }
.referral-stats b { color:#E6D5A8; }
.referral-tiers { display:flex; flex-direction:column; gap:6px; margin:6px 0 12px; }
.rtier { display:flex; align-items:center; gap:8px; flex-wrap:wrap; padding:8px 10px;
         border-radius:2px; background:#0D0F13; font-size:12px; }
.rtier.stub { opacity:.55; }
.rtier-k { color:#C3B48E; flex:1; min-width:120px; }
.rtier-reward { font-weight:700; }
.rtier-ready { color:#262C36; font-weight:700; }
.rtier-wait { color:#83764F; }
.rtier-stub { color:#83764F; font-style:italic; }
.referral-none { margin-top:8px; text-align:center; color:#A99A70; font-size:12px; }

.cmp { display:flex; gap:9px; margin-top:12px; }
.cmpcard { flex:1; border-radius:2px; padding:9px; background:#0D0F13; border:1px solid #262C36; font-size:12px; }
.cmpcard.new { border-color:#E6D5A8; box-shadow:0 0 12px #E6D5A833; }
/* Empty-slot compare card (P1 FIX #11): equipping an unbound item into an empty slot
   now always shows the compare panel (owner ruling — see app.js's showCompare/
   cmpCardHtml); the "before" side has nothing to render, so this dashed/dimmed variant
   stands in for a real item card instead of leaving a blank box. */
.cmpcard.cmpcard-empty { border-style:dashed; opacity:.7; }
.cmpcard h5 { margin:0 0 3px; font-size:12px; }
.cmpcard .sub { font-size:10px; margin-top:1px; }
.cmpcard .body { margin-top:5px; font-size:11.5px; line-height:1.5; }
.cmpcard .pwr { margin-top:5px; color:#E6D5A8; font-weight:700; font-size:12px; }
.delta { margin-top:12px; background:#0A0B0E; border-radius:2px; padding:10px 12px; }
.delta div { display:flex; justify-content:space-between; padding:4px 0; font-size:13px; }
/* Owner display law: compare panel ONLY — upgrades green, downgrades red, unchanged
   gray. Red is reserved for true negatives (a stat that went down); it never
   appears on the item sheet's own affix values (see .affrow .v above). */
.up { color:#E6D5A8; font-weight:700; } .dn { color:#C4553E; font-weight:700; } .neu { color:#6A6E77; }
.bindwarn { margin-top:12px; text-align:center; background:#262C35; border:1px solid #E6D5A844;
            color:#E6D5A8; border-radius:2px; padding:9px 12px; font-size:12px; }
.confirm { margin-top:10px; background:linear-gradient(180deg,#B24A38,#7A2E23); color:#F2E4CC;
           text-align:center; border-radius:2px; padding:12px 0; font-weight:800; font-size:14px;
           letter-spacing:.05em; cursor:pointer; }
.confirm.dis { background:#191D24; color:#6A6E77; cursor:default; }
.locknote { text-align:center; margin-top:6px; color:#C4553E; font-size:12px; font-weight:600; }
/* T2.7 (modal grammar standard): Cancel is now ALWAYS a visible, bordered secondary
   button — same weight as `.b-sl` — instead of the plain text link this used to be.
   Two contexts: (1) inside a `.btns` flex row (showRenameModal/showSellConfirm/
   showSaveModal, via cancelBtnHtml()), where `.btns div` (more specific: class+tag)
   already wins the shared box properties (flex:1/padding/border-radius/font-size), so
   only the background/border/color/font-weight below actually apply there — the
   `.btns .cancel` rule underneath zeroes the standalone margin-top so it doesn't sit
   lower than its sibling button in the flex row; (2) standalone as a full-width block
   (showCompare, directly below the confirm/locknote — not wrapped in `.btns`), where
   this rule's own padding/border-radius/margin-top make it read as a real button on
   its own line. */
.cancel { display:block; text-align:center; margin-top:7px; padding:10px 0;
          border-radius:2px; background:#191D24; border:1px solid #353C46;
          color:#C3B48E; font-size:12px; font-weight:700; cursor:pointer; }
.btns .cancel { margin-top:0; }
/* The save/login modal's mode-switch line ("New here?" / "Already have an account?")
   is NOT a Cancel action — kept as the old plain-text-link look `.cancel` used to have,
   under its own class so restyling `.cancel` into a bordered button (above) can't
   accidentally turn this into one too. */
.modeswitch { text-align:center; margin-top:7px; color:#6A6E77; font-size:12px; cursor:pointer; }
/* T2.7: inline modal error — renders ABOVE the `.btns` row (never below/after), same
   red/alarm palette as #toast's default (error) state, boxed like `.bindwarn` so it
   reads as a distinct notice rather than blending into `.imeta` body copy. */
.modalerr { margin-top:10px; text-align:center; background:#2a1414; border:1px solid #C4553E55;
            color:#C4553E; border-radius:2px; padding:9px 12px; font-size:12px; font-weight:600; }

/* Bind-on-equip hint under the item-sheet action row (unbound items only). Amber, not
   alarming red — it's informational, matching .bindwarn's palette. */
.bindhint { margin-top:9px; text-align:center; color:#E6D5A8; font-size:11px; opacity:.85; }

/* FIX #17 (P2, ban native dialogs): the in-game sell-confirm modal's payout line
   (showSellConfirm, app.js) — reuses .frcard for the item preview row above this. */
.sellpayout { margin-top:10px; text-align:center; font-size:14px; color:#C3B48E; }
.sellpayout b { color:#E6D5A8; font-size:18px; }

/* ---- Forge Screen (T23): Craft · Reroll (with locks) · Trader ---------------------
   .tabs2/.btn2 lifted from docs/mockups/screens-layout.html's sub-tab nav + secondary
   button, scaled from that file's thumbnail (e.g. 4px tab padding) to real mobile
   density — same "direction, not literal thumbnail pixels" adaptation every other
   mockup port in this file already applies (doll, item sheet, compare). */
.tabs2 { display:flex; gap:6px; margin-bottom:12px; }
.tabs2 div { flex:1; text-align:center; padding:9px 0; border-radius:2px; background:#0E1014;
             border:1px solid #262C36; color:#83764F; font-size:13px; cursor:pointer; }
.tabs2 .on { background:#1F242C; color:#E6D5A8; border-color:#3A4250; font-weight:700; }

.btn2 { background:#191D24; border:1px solid #353C46; color:#C3B48E; text-align:center;
        border-radius:2px; padding:9px 12px; font-weight:600; font-size:13px; cursor:pointer; }
/* FIX #15/#16 (P2): shortfall/blocked state — "disabled + RED" (craft's "need N more",
   trader's unaffordable BUY / bound-item SELL). Disabled buttons in this state carry NO
   data-act (see craftBaseCardHtml/renderTraderTab in app.js) — structurally inert, not
   just visually dimmed, so no click-guard logic is needed anywhere this class appears. */
.btn2.dis { background:#191D24; border-color:#C4553E44; color:#C4553E; opacity:.7; cursor:default; }

/* Forge item/result cards (craft's result + the reroll tab's selected-item header)
   reuse the r-<rarity> border/color language (T18) but need their OWN block/icon
   rules: T21's near-identical .dr/.ic2/.pw/.afx are deliberately scoped under #reveal
   (results-reveal only) — duplicating the handful of rules here rather than
   broadening #reveal's own selectors, so this task cannot alter that already-shipped
   screen's cascade behavior. Reuses the SAME popin/flipin/glow* keyframes T21 defined
   (keyframes are global regardless of which rule first declares them). */
.frcard { border-radius:2px; padding:9px 10px; background:#0E1014; border:1px solid #262C36;
          display:flex; gap:9px; align-items:center; margin-top:10px; }
.frcard .fric { width:32px; height:32px; border-radius:2px; background:#191D24;
                display:flex; align-items:center; justify-content:center; font-size:17px; flex:none; }
.frcard .frpw, .frcard .afx { color:#A99A70; font-size:11.5px; font-weight:400; }
/* FIX #15 (P2, Craft tab): craft results now also carry the reveal's own #12
   cardpulse/cardpulseBig border-pulse (see #reveal .dr.play.r-<rarity> above) on top
   of the existing popin/flipin/glow* landing — "the full loot-reveal grammar," not a
   plain card. Durations match the reveal's own per-rarity numbers exactly. */
.frcard.play.r-common { animation:popin .18s ease-out forwards; }
.frcard.play.r-uncommon { animation:popin .25s ease-out forwards, flashg .55s ease, cardpulse .5s ease-out 1; }
.frcard.play.r-rare { animation:flipin .5s cubic-bezier(.2,1.4,.4,1) forwards, glowb 1.3s ease, cardpulse .55s ease-out 2; }
.frcard.play.r-epic { animation:flipin .5s cubic-bezier(.2,1.4,.4,1) forwards, glowp 1.4s ease, cardpulse .6s ease-out 2; }
.frcard.play.r-legendary { animation:flipin .55s cubic-bezier(.2,1.4,.4,1) forwards, glowo 1.9s ease, cardpulseBig .7s ease-out 2; }
.frcard.play.r-mythic { animation:flipin .6s cubic-bezier(.2,1.4,.4,1) forwards, glowr 2.4s ease, cardpulseBig .85s ease-out 3; }
.frcard.play.r-gamebreaking { animation:flipin .6s cubic-bezier(.2,1.4,.4,1) forwards; }

/* Craft tab — FIX #15 (P2, desire pass): each discovered base is a full card (slot +
   name, level req, implicit range, pitch line, scrap progress bar), not a single-line
   row. .craftbar reuses the header strip's .ebar progress-bar language (track + fill
   div), just its own class so this task can't alter that already-shipped strip. */
/* #4 (P2, cert Day-1 batch T2): the tab-level orientation line (renderCraftTab),
   once, above every per-base card — distinct from each card's own .crpitch below
   (per-base detail), same muted/quiet tone as .tradernote/.stub. */
.crafttabcopy { color:#83764F; font-size:12px; line-height:1.45; margin-bottom:10px; }
/* M1.9 item 18 — the craft-odds chip row: the full rarity distribution the forge rolls
   against, one colored chip per possible rarity, straight from the server's craft_odds
   (--rc is the rarity's own swatch, data/rarities.toml). */
.craftodds { display:flex; flex-wrap:wrap; align-items:center; gap:6px; margin-bottom:12px; }
.craftodds-label { color:#6A6E77; font-size:10.5px; letter-spacing:.06em;
                   text-transform:uppercase; margin-right:2px; }
.craftodd { display:inline-flex; align-items:center; gap:4px; font-size:11px; color:#C9B98E;
            background:#0E1014; border:1px solid #262C36; border-left:3px solid var(--rc,#E6D5A8);
            border-radius:2px; padding:2px 7px; white-space:nowrap; }
.craftodd b { color:var(--rc,#E6D5A8); font-weight:600; }
/* M1.9 item 15 — the daily earned/cap dust line demoted onto the Forge. */
.dustearned { color:#6A6E77; }
.craftcard { background:#0E1014; border:1px solid #262C36; border-radius:2px;
             padding:10px 12px; margin-top:8px; }
.craftrow-top { display:flex; align-items:center; justify-content:space-between; gap:8px; }
.crname { color:#E6D5A8; font-size:13px; font-weight:600; }
.crlvl { color:#6A6E77; font-size:11px; white-space:nowrap; }
.crimplicit { color:#B2A379; font-size:11.5px; margin-top:5px; }
.crimplicit b { color:#E6D5A8; }
.crpitch { color:#83764F; font-size:11.5px; font-style:italic; margin-top:3px; }
.craftbarrow { display:flex; align-items:center; gap:8px; margin-top:8px; }
.craftbar { flex:1; height:7px; background:#191D24; border-radius:4px; overflow:hidden; }
.craftbar>div { height:100%; background:linear-gradient(90deg,#262C36,#E6D5A8); }
.craftbarlabel { color:#83764F; font-size:11px; white-space:nowrap; }
.craftbtn { margin-top:9px; width:100%; box-sizing:border-box; }

/* Reroll tab — the centerpiece. .affrow/.qbar/.qlab (T22, global) are reused verbatim
   for each line; .forgerow/.locktoggle/.spin/.landed are the only new rules needed. */
/* 6a 2-col picker: rows reuse the trader-row card language; the selected accent is BRONZE
   (never a rarity hue as decoration — the ratified reservation rule). Base = stacked
   (mobile); the 300px·1fr grid lives in the >=1024 block below. */
.fpickrow { display:flex; align-items:center; gap:9px; background:#0E1014;
            border:1px solid #262C36; border-radius:2px; padding:9px 10px;
            margin-bottom:8px; cursor:pointer; }
.fpickrow.on { border-color:#8A6A2E; background:#12151b; }
.fpicklist { margin-bottom:10px; }
/* Item 5 (popup kill): the top-bar '?' — the one durable-help home — and the codex panel.
   Chrome greys; the moment-of-need lock hint is quiet inline text, never a popup. */
#helpBtn { position:fixed; top:9px; right:44px; z-index:60; width:26px; height:26px;
           background:#12161d; color:#8a929e; border:1px solid #2a3140; border-radius:50%;
           font-size:14px; cursor:pointer; line-height:1; }
.codex .codexline { font-size:12.5px; color:#C3B48E; margin:7px 0; line-height:1.45; }
.codex .codexline.dim { color:#6A6E77; font-style:italic; }
.lockhint { font-size:11.5px; color:#83764F; font-style:italic; margin:8px 0; }

/* FEN WAR phase 2: the claim page's war section — chrome greys + the standing .bigbtn
   language; never a rarity hue. */
/* Iconography v1 (ticket1 §7a): monoline glyphs inherit colour from context. Default
   --body; the earned-feat rosette is the single named EMBER exception (guardrails block,
   owner-amended); reward markers bronze; the found-item gem takes the row's r-* rarity
   class (rarity-scoped — reservation-guard clean). */
.feedicon svg { width:17px; height:17px; vertical-align:-3px; }
.feedicon { color:#C3B48E; }
.feedicon.ico-rosette { color:var(--ember); }
.rwico { color:var(--bronze); display:inline-flex; vertical-align:-3px; }
.rwico svg { width:16px; height:16px; }
.warline { font-size:12.5px; color:#C3B48E; margin:8px 0; }
.warline.dim { color:#6A6E77; font-style:italic; }
.warcommit { display:flex; flex-direction:column; gap:8px; margin-top:8px; }
.warcommit select, .warcommit input { background:#0E1014; color:#C3B48E;
  border:1px solid #262C36; border-radius:2px; padding:7px 9px; font-size:13px; }
.warcommit input { width:64px; }
/* Phase 3 fog: the claim page's two fog actions — quiet secondaries under the muster
   (the primary decision stays COMMIT/DECLARE), iron-framed, no reserved hues. */
.warfog { display:flex; flex-direction:column; gap:8px; margin-top:10px; }
.warfog .btn2 { width:100%; }
/* Phase 4 — DRAGGED UNDER: the injury banner. ASH treatment (the DEFEAT law — never
   red, red is Mythic's): iron panel, ash-grey left rule, parchment body. */
.draggedunder { border:1px solid #262C36; border-left:3px solid #6A6E77; border-radius:2px;
                background:#0E1014; padding:10px 12px; margin-bottom:12px; }
.draggedunder .sectionlabel { color:#8a8f99; margin-bottom:4px; }
@media (min-width: 1024px) {
  .forgewrap { display:grid; grid-template-columns:300px 1fr; gap:16px; align-items:start; }
  .fpicklist { max-height:calc(100vh - 220px); overflow-y:auto; margin-bottom:0; }
}
.forgerow { position:relative; padding-right:40px; }
.locktoggle { position:absolute; top:8px; right:9px; background:#191D24; border:1px solid #353C46;
              border-radius:2px; width:26px; height:24px; font-size:12px; color:#C3B48E; cursor:pointer; }
.locktoggle.on { background:#212730; border-color:#8A6A2E88; }
/* Lock-proof free reroll (P1 FIX #10.5): while the free reroll is available, every lock
   toggle renders `disabled` (app.js's forgeAffixRowHtml) — visually dimmed here to match
   the existing .btns .dis / .confirm.dis "can't touch this" language elsewhere in the
   sheet, so the player reads it as inert on sight, not just on a failed tap. */
.locktoggle:disabled, .locktoggle.dis { opacity:.35; cursor:default; }
.forgerow.locked { opacity:.75; }
.forgerow.locked .qbar { opacity:.6; }
@keyframes slotblur { 0%,100% { opacity:.35; filter:blur(1.5px); } 50% { opacity:.65; filter:blur(2.5px); } }
.forgerow.spin { animation:slotblur .18s ease-in-out infinite; pointer-events:none; }
.forgerow.landed { animation:flipin .4s cubic-bezier(.2,1.4,.4,1); }
.freebanner { text-align:center; background:#0F1319; border:1px solid #262C3655; color:#C3B48E;
              border-radius:2px; padding:8px 10px; font-size:12.5px; margin:10px 0 4px; }
.rrcost { text-align:center; color:#83764F; font-size:12px; margin:10px 0 2px; }
.rrcost b { color:#E6D5A8; }
/* FIX #14 (P2, reroll decision theater): SHORTFALL-RED wallet + "hunt for more dust →"
   link when the player can't afford the current (locked-in) cost — red reserved for
   true negatives, same owner display law the compare panel's ▼ downgrades already use. */
.rrcost.shortfall b { color:#C4553E; }
.huntlink { color:#E6D5A8; text-decoration:none; }
/* FIX #14: frames an engaged lock as a strategic trade-off, not just a cost bump —
   purple accent matches .freebanner's own "special reroll state" language. */
.lockframe { text-align:center; color:#C3B48E; font-size:12px; margin:8px 0 2px; }
.lockframe b { color:#E6D5A8; }
.alllocked-note { text-align:center; color:#C4553E; font-size:12px; margin:10px 0 2px; font-weight:600; }
/* Ratified breach fix (5a): reroll is FORGED BRONZE, not purple. Purple is Epic-only;
   dust is the diamond glyph (the currency), never the button colour. Same shared primary. */
.bigbtn.reroll { background:linear-gradient(180deg,#171B22,#0E1014); color:var(--ember);
                 border-color:#A9852F; box-shadow:var(--ember-glow); }

/* ---- Mine tab (Task 3.A, M1.6-A client) --------------------------------------------
   Node/tier picker reuses .zonelist/.zcard/.tier verbatim (Camp screen, above) — zero
   new rules needed there. Strike/dig/cave-in/bank result cards reuse .frcard (Forge,
   below) and .log (results reveal, above). The only genuinely new chrome is the live
   at-risk-pot readout below — a distinct mining-specific glow/shake/color language
   (e.g. an amber "at risk" treatment vs. a green "safe to bank" one) is a Wave 4
   visual-polish item, not built here; this is the minimal functional readout. */
.minepot { margin-top:10px; background:#0E1014; border:1px solid #262C36; border-radius:2px;
           padding:10px 12px; }
.minepot-label { color:#83764F; font-size:11px; letter-spacing:.08em; }
.minepot-value { margin-top:4px; color:#E6D5A8; font-weight:700; font-size:14px; }
.mine-caved { border-color:#C4553E55; }
.mine-caved .fric { color:#C4553E; }
.mine-banked { border-color:#E6D5A855; }

/* Trader tab */
.traderow { display:flex; align-items:center; gap:9px; background:#0E1014; border:1px solid #262C36;
            border-radius:2px; padding:9px 10px; margin-top:8px; }
.traderow .fric { width:30px; height:30px; border-radius:2px; background:#191D24; display:flex;
                   align-items:center; justify-content:center; font-size:15px; flex:none; }
.traderow > div:nth-child(2) { flex:1; font-size:12.5px; }
.buyrow { display:flex; align-items:center; gap:8px; margin-top:8px; }
.stepbtn { width:32px; height:32px; background:#191D24; border:1px solid #353C46; border-radius:2px;
           color:#C3B48E; font-size:16px; cursor:pointer; }
.qtynum { min-width:22px; text-align:center; font-weight:700; color:#E6D5A8; }
.buybtn { flex:1; }
.tradernote { text-align:center; color:#6A6E77; font-size:11.5px; margin-top:12px; }

/* ---- FTUE header chips + Save/Login modal (T23) -------------------------------------
   #chips is created at runtime (ensureChipsEl() in app.js), a sibling of #strip — the
   same "no index.html edit needed" convention T21/T22 already use for #reveal/
   #itemModal. #chips:empty collapses the row to zero height when neither chip applies,
   rather than leaving a bare padding gap under the header strip. */
#chips:empty { display:none; }
#chips { display:flex; flex-direction:column; gap:6px; padding:8px 14px 0; background:#0E1014; }
.chip { display:flex; align-items:center; justify-content:space-between; gap:8px;
        background:#181C24; border:1px solid #2E343E; border-radius:2px; padding:8px 12px;
        font-size:12.5px; color:#C3B48E; cursor:pointer; }
.savechip { border-color:#8A6A2E; color:#C4A96A; }
.forgenudge { justify-content:center; border-color:#262C3655; color:#C3B48E; }
.chipx { color:#83764F; font-size:13px; padding:0 2px; }
/* R2b (docs/REACHABILITY-MATRIX.md): the claimed-account counterpart to .savechip —
   mutually exclusive with it (app.js's renderChips() shows .savechip for a guest,
   .acctchip for a claimed account, never both), same header-chip language, its own
   neutral blue so it doesn't read as an alert/nudge the way .savechip's gold or
   .forgenudge's purple do. Opens showSaveModal("account") -> LOG OUT. */
.acctchip { justify-content:center; border-color:#8A6A2E55; color:#C4A96A; }

/* ---- FTUE equip-hint (P1 FIX #10.2) --------------------------------------------------
   One-time "equip your new [name] →" pointer on the Character screen for the hunt-3
   scripted-rare ceremony item, plus a matching pulse highlight on its inventory cell —
   both auto-clear the instant the item is equipped or sold (app.js's ftueEquipHint()),
   and the pointer has its own ✕ dismiss, same .chip/.chipx language as the header chips
   above even though this one lives inside the Character screen's own #screen, not
   #chips (see renderCharacter()/wireCharacter() in app.js). Reuses T21's own `pulse`
   keyframe rather than inventing a new one. */
.chip.ftuehint { margin:12px 0 4px; border-color:#E6D5A855; color:#E6D5A8; }
.chip.ftuehint b { color:#E6D5A8; }
.cell.ftue-glow { animation:pulse 1.4s ease infinite; box-shadow:0 0 0 2px #E6D5A8aa, 0 0 14px #E6D5A866; }
.saveinput { width:100%; box-sizing:border-box; background:#0A0B0E; border:1px solid #262C36;
             color:#E6D5A8; border-radius:2px; padding:10px 11px; font-size:13px; margin-top:8px; }

/* ---- Gamebreaking (T23) — glitch/prismatic treatment, SPEC.md §5's 7th rarity -------
   rarities.toml ships "gamebreaking" with weight=0 (zero items in M1; first items
   arrive at M2's open-beta event per CLAUDE_ROADMAP.md). This makes the client READY
   for that day without any item using it yet: .r-gamebreaking slots into every
   existing r-<rarity> call site (T18's loot/inventory cards, T22's item-sheet/compare,
   T23's forge cards) exactly the way .r-mythic etc. already do, and app.js's
   itemNameHtml() (T23) wraps a gamebreaking item's name in .gtxt at every
   name-rendering site. .gbchip/.gtxt/@keyframes hue,jit are lifted VERBATIM from
   docs/mockups/gamebreaking-and-lock.html (the approved 7th-rarity mockup); only the
   .r-gamebreaking/.s-gamebreaking glue rules are new, extending (not duplicating) that
   verbatim treatment onto the existing r-<rarity>/s-<rarity> ladders. */
@keyframes hue { to { filter:hue-rotate(360deg); } }
@keyframes jit { 0%,92%,100% { clip-path:inset(0); } 93% { clip-path:inset(15% 0 40% 0); transform:translate(2px,0) skewX(2deg); } 95% { clip-path:inset(60% 0 10% 0); transform:translate(-2px,0); } 97% { clip-path:inset(30% 0 45% 0); } }
.gbchip { position:relative; background:#0a0d16; color:#fff; overflow:hidden; }
.gbchip::before { content:''; position:absolute; inset:0; border-radius:2px; padding:1.5px;
  background:linear-gradient(115deg,#ff5ef7,#5efcff,#fffb5e,#5eff8f,#ff5ef7);
  -webkit-mask:linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
  -webkit-mask-composite:xor; mask-composite:exclude; animation:hue 3s linear infinite; }
.gtxt { position:relative; display:inline-block; animation:jit 4s steps(1) infinite; }
.gtxt::before, .gtxt::after { content:attr(data-t); position:absolute; left:0; top:0; width:100%; }
.gtxt::before { color:#5efcff; transform:translate(-1.2px,-.6px); mix-blend-mode:screen; animation:jit 3.1s steps(1) infinite reverse; }
.gtxt::after { color:#ff5ef7; transform:translate(1.2px,.6px); mix-blend-mode:screen; animation:jit 2.7s steps(1) infinite; }

.r-gamebreaking { position:relative; overflow:hidden; color:#fff; border-color:#ff5ef7; }
.r-gamebreaking::before { content:''; position:absolute; inset:0; border-radius:inherit; padding:1.5px;
  background:linear-gradient(115deg,#ff5ef7,#5efcff,#fffb5e,#5eff8f,#ff5ef7);
  -webkit-mask:linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
  -webkit-mask-composite:xor; mask-composite:exclude; animation:hue 3s linear infinite; pointer-events:none; }
.s-gamebreaking { border-color:#ff5ef7; color:#fff; position:relative; overflow:hidden;
                   box-shadow:inset 0 0 10px #000000aa, 0 0 14px #ff5ef755; }
.s-gamebreaking::before { content:''; position:absolute; inset:0; border-radius:inherit; padding:1.5px;
  background:linear-gradient(115deg,#ff5ef7,#5efcff,#fffb5e,#5eff8f,#ff5ef7);
  -webkit-mask:linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
  -webkit-mask-composite:xor; mask-composite:exclude; animation:hue 3s linear infinite; pointer-events:none; }
.s-gamebreaking small { color:#A99A70; }

/* ---- T2.3 (Landing/intro): splash -> ENTER DURNVALE -> world-set card -> Camp -------
   A brand-new visitor (boot()'s refresh() found no session yet) sees this BEFORE any
   guest account is ever created (app.js's enterDurnvale() only fires on ENTER/Skip, not
   on boot) — CSS-only vignette + title treatment, no image asset (art arrives later per
   the brief). Body-appended full-screen panel, same "own id, above everything, torn
   down by remove()" convention #reveal/#itemModal already use (T20-23); z-index:300
   clears every existing overlay (#toast's 200 is the next-highest) though in practice
   nothing else is ever on-screen yet the moment this shows. A returning visitor never
   sees any of this — boot()'s refresh() already succeeded and render() ran normally. */
#splash { position:fixed; inset:0; z-index:300; background:#080A0D; color:#C3B48E;
          display:flex; align-items:center; justify-content:center; padding:24px;
          overflow-y:auto; }
/* ART (Post-Pass-7 art pass): the landing splash image, full-bleed cover. Wide 16:9
   by default; the 9:16 portrait crop swaps in on narrow (mobile) viewports so the
   figure/moon stay framed instead of edge-cropped. */
.splash-art { position:absolute; inset:0; pointer-events:none;
              background:#080A0D url("art/processed/landing-splash-wide-final.webp") center/cover no-repeat; }
@media (max-width:640px) {
  .splash-art { background-image:url("art/processed/landing-splash-portrait.webp"); }
}
/* The scrim over the art: dark at top (title) and bottom (button), clear through the
   middle band so the moon + lantern-bearer read. Replaces the old CSS-only vignette. */
.splash-vignette { position:absolute; inset:0; pointer-events:none;
                    background:linear-gradient(180deg, #080A0De6 0%, #080A0D66 22%,
                              #080A0D00 46%, #080A0D33 66%, #080A0Df2 100%); }
.splash-inner { position:relative; z-index:1; width:100%; max-width:340px; text-align:center;
                display:flex; flex-direction:column; justify-content:flex-start; }
@keyframes splashglow { 0%,100%{text-shadow:0 0 18px #E7A83E55, 0 0 42px #8A6A2E33}
                         50%{text-shadow:0 0 30px #C4553E99, 0 0 60px #8A6A2E55} }
.splash-title { font-size:42px; font-weight:800; letter-spacing:.22em; color:#F0E2C0;
                 font-family:var(--serif);
                 animation:splashglow 3.2s ease-in-out infinite; }
.splash-rule { width:64px; height:2px; margin:14px auto; background:linear-gradient(90deg,transparent,#C4553E,transparent); }
/* Post-Pass-6 fix 5 (D038): the splash subtitle joins the serif world-voice register
   (.voiceline's Georgia/Times italic, D031) — the title above was already serif; the
   subtitle was the one sans line left on the landing surface. */
.splash-sub { color:#A2946C; font-size:13.5px; letter-spacing:.03em; line-height:1.6;
              margin-bottom:26px; font-family:var(--serif);
              font-style:italic; }
.splash-enter { max-width:280px; margin-left:auto; margin-right:auto; }
.splash-skip { margin-top:16px; color:#6A6E77; font-size:12px; text-decoration:underline; cursor:pointer; }
/* Desktop landing (wide art): the title + ENTER block sits over the treated left third
   (the processed art darkens the left + bottom to near-black for exactly this), with the
   teal-cloaked figure holding the right. A left-edge scrim guarantees parchment contrast
   independent of the baked-in treatment. Mobile (portrait art, <=640px) stays centered. */
@media (min-width:641px) {
  #splash { justify-content:flex-start; }
  .splash-vignette { background:
      linear-gradient(90deg, #080A0Df2 0%, #080A0Dd9 26%, #080A0D73 44%, #080A0D00 62%),
      linear-gradient(180deg, #080A0D99 0%, #080A0D00 30%, #080A0D00 68%, #080A0Df2 100%); }
  .splash-inner { margin-left:max(6vw,32px); max-width:400px; text-align:left; align-items:flex-start; }
  .splash-title { font-size:clamp(46px,6vw,60px); }
  .splash-rule { margin-left:0; margin-right:auto; }
  .splash-enter { margin-left:0; margin-right:0; }
}

/* T2.3: the 2-3 line world-set flavor card, shown only after ENTER DURNVALE actually
   creates the guest (never after the splash's own "Skip intro" link) — same
   body-appended overlay pattern as #splash, its own id so the two can never overlap
   (removeSplashPanel() always tears #splash down first, before this is built). */
#worldcard { position:fixed; inset:0; z-index:300; background:#080A0De8; color:#C3B48E;
             display:flex; align-items:center; justify-content:center; padding:24px; }
.worldcard-inner { width:100%; max-width:340px; text-align:center; background:#0E1014;
                    border:1px solid #262C36; border-radius:3px; padding:28px 22px; }
/* Post-Pass-6 fix 5 (intro modal serif, D038): the world-set flavor lines speak in the
   serif world-voice register (.voiceline/.omenline, D031/D034 — Georgia/Times italic)
   — the intro IS the game's first lore beat and read in the default sans until now.
   The final line ("Hunt well.") keeps a firmer tone via :last-of-type weight. The
   desktop @media block's sans reset no longer covers this selector (see line ~149).
   ART SLOT (owner order, reserved): the splash/world-card stays CSS-vignette-only —
   when the owner's splash art drops, it follows the wanderer.svg standby convention
   (standalone file + <img> anchor here; no pipeline built ahead of the asset). */
.worldcard-lines p { margin:0 0 10px; font-size:15.5px; color:#C9B98E; line-height:1.6;
                     font-family:var(--serif); font-style:italic; }
.worldcard-lines p:last-of-type { color:#E6D5A8; font-weight:600; }
.worldcard-lines p:last-child { margin-bottom:22px; }

/* T2.3: one-time HUNT button glow the instant a brand-new visitor lands on Camp out of
   the intro sequence (app.js renderCamp()'s S.firstHuntGlow) — cleared the moment
   hunt() actually fires, so it never lingers past the player's first tap. */
@keyframes huntglow { 0%,100%{box-shadow:0 0 14px #E7A83E55} 50%{box-shadow:0 0 26px 6px #E7A83Eaa} }
.bigbtn.glow-intro { animation:huntglow 1.6s ease-in-out infinite; }

/* Task 5 (cert Day-1 #6, discoverability coach-marks): a subtle one-shot pulse on a
   NEW verb's nav entry the first time it becomes available (app.js's wireTabs()/
   coachMarkEligible() — Mine after hunt 5, Feats after the first feat is earned).
   Deliberately understated next to glow-intro's bigger red HUNT-button treatment
   above — a nav-rail hint drawing the eye, not a fanfare — a soft label-color pulse
   plus a small fixed dot (the dot alone stays visible between animation troughs, so
   the hint reads even to a player who glances at a "resting" frame). Same #tabs
   button element at every breakpoint (mobile bottom-bar vs >=1024px vertical rail —
   see the base #tabs button rule above), so this needs no width-specific override
   beyond repositioning the dot for the rail's own larger padding. */
@keyframes navcoachpulse { 0%,100%{color:#6A6E77} 50%{color:#E6D5A8; text-shadow:0 0 8px #E6D5A8aa} }
#tabs button.coachmark { position:relative; animation:navcoachpulse 1.8s ease-in-out infinite; }
#tabs button.coachmark::after {
  content:''; position:absolute; top:6px; right:8px; width:6px; height:6px;
  border-radius:50%; background:#E6D5A8; box-shadow:0 0 6px 2px #E6D5A8aa; pointer-events:none;
}
@media (min-width: 1024px) {
  #tabs button.coachmark::after { top:12px; right:16px; }
}

/* M1.6-C Feats tab (achievements v1) — .craftcard's own layout language (name/reward
   row, description line, progress bar via the shared .craftbarrow/.craftbar/
   .craftbarlabel classes, a full-width action row) reused for a card family here so
   the app's two progress-bar surfaces (Craft's scrap cost, Feats' metric progress)
   read as one visual system rather than two. Functional-only pass — a claim
   celebration (glow/sound on a fresh CLAIM, a trophy-case sort) is flagged for Wave 4
   visual polish, not built here. */
.featcard { background:#0E1014; border:1px solid #262C36; border-radius:2px;
            padding:10px 12px; margin-top:8px; }
.featcard-claimed { opacity:.6; }
.featrow-top { display:flex; align-items:center; justify-content:space-between; gap:8px; }
.featname { color:#E6D5A8; font-size:13px; font-weight:600; }
.featreward { color:#E6D5A8; font-size:11.5px; white-space:nowrap; }
.featdesc { color:#83764F; font-size:11.5px; margin-top:3px; }
.featbtn { margin-top:9px; width:100%; box-sizing:border-box; }
.featclaimed { margin-top:9px; width:100%; box-sizing:border-box; text-align:center;
               padding:9px 0; border-radius:2px; background:#132a1e; border:1px solid #262C3644;
               color:#262C36; font-size:13px; font-weight:700; }

/* M1.7 activity feed v1 (docs/superpowers/specs/m1.7-presence.md §1) — a read-only,
   GLOBAL strip of noteworthy moments. .feeditem reuses .featcard's own card language
   (background/border/radius) at a slimmer row height, since a feed row is a single
   line rather than a multi-field card. */
.feedheader { display:flex; align-items:center; justify-content:space-between; gap:8px; }
.feedrefresh { width:auto; padding:6px 12px; font-size:11px; }
.feedlist { margin-top:8px; }
.feeditem { background:#0E1014; border:1px solid #262C36; border-radius:2px;
            padding:8px 12px; margin-top:6px; display:flex; align-items:center; gap:9px; }
.feedicon { font-size:15px; flex:none; }
.feedtext { color:#C3B48E; font-size:12.5px; flex:1; }
.feedtext b { color:#E6D5A8; }
.feedwhen { color:#6A6E77; font-size:10.5px; white-space:nowrap; flex:none; }

/* M1.8 social spine A1 (docs/superpowers/specs/m1.8-social-spine.md) — the feed's own
   half of "every character NAME anywhere is a CLICKABLE doorway to the profile"
   (app.js's feedWhoHtml). Same color as the surrounding bold text (.feedtext b above)
   by design — a profile link should read as "this IS the name," not a jarring
   different-colored anchor — with a dotted underline as the only click affordance. */
.profilelink { color:inherit; text-decoration:none; border-bottom:1px dotted #6A6E77; cursor:pointer; }
.profilelink:hover { border-bottom-color:#83764F; }

/* M1.8 social spine A1 — Player Profile (app.js's showProfile/buildProfileHtml). Reuses
   #itemModal/.modal-backdrop/.modal-card/.isheet (every other in-game modal in this
   file) AND the Character screen's own .dollwrap/.slotf/.charname/.stats classes
   verbatim for the paper-doll + stat grid — only the join-date line below the stat
   grid is new here. */
.profile-joined { text-align:center; color:#6A6E77; font-size:11px; margin:8px 0 14px; }

/* M1.9 item 21 — Universal Character Page v2 (app.js's buildProfileHtml, DECISIONS D030):
   the class line, the PvP record, lifetime highlights, the feats list, and the opt-in
   display case. All sit between the stat grid and the join-date line, reusing the modal's
   dark palette + .sectionlabel/.stub2/.r-<rarity> conventions. */
.profile-class { text-align:center; color:#83764F; font-size:11px; letter-spacing:.12em;
                 text-transform:uppercase; margin-top:2px; }
.profile-record { display:flex; align-items:center; justify-content:center; gap:6px;
                  margin:12px 0 4px; font-size:12.5px; color:#C3B48E; }
.profile-record b { color:#E6D5A8; }
.profile-record .prec-sep { color:#353C46; }
.profile-record .prec-k { color:#E6D5A8; }
.profile-record .prec-d { color:#C4553E; }
.profile-highlights { display:flex; flex-direction:column; gap:3px; align-items:center;
                      margin:6px 0; font-size:11.5px; color:#A2946C; }
.profile-highlights .phl-k { color:#6A6E77; text-transform:uppercase; letter-spacing:.08em;
                             font-size:10px; margin-right:5px; }
.profile-feats { margin:12px 0; }
.featchips { display:flex; flex-wrap:wrap; gap:6px; justify-content:center; margin-top:6px; }
.featchip { background:#181C24; border:1px solid #2E343E; border-radius:2px;
            padding:3px 9px; font-size:11px; color:#C3B48E; }
.profile-case { margin:12px 0; }
.profile-case .dc-header { display:flex; align-items:center; justify-content:space-between;
                           gap:8px; margin-bottom:6px; }
.dc-manage { font-size:10.5px; padding:5px 8px; }
.dcgrid { display:flex; flex-wrap:wrap; gap:8px; }
.dccard { display:flex; flex-direction:column; align-items:center; text-align:center;
          background:linear-gradient(180deg,#0D0F13f5,#0a0e1af5); border:1px solid #2E343E;
          border-radius:2px; padding:8px 10px; font-size:11px; color:#C3B48E; min-width:80px;
          box-shadow:inset 0 0 10px #000000aa; }
.dccard small { display:block; color:#6A6E77; font-size:9px; margin-top:2px; }
/* The curation modal (app.js's showDisplayCaseModal): a grid of toggle-picks. */
.dcmodal { text-align:center; }
.dcmodal-hint { color:#83764F; font-size:11.5px; margin:4px 0 12px; }
.dcpickgrid { display:flex; flex-wrap:wrap; gap:8px; justify-content:center; margin-bottom:14px; }
.dcpick { background:#0D0F13; border:1px solid #2E343E; border-radius:2px; padding:8px 10px;
          font-size:11px; color:#A2946C; cursor:pointer; min-width:80px; }
.dcpick small { display:block; color:#6A6E77; font-size:9px; margin-top:2px; }
.dcpick.on { border-color:#C4A96A; color:#E6D5A8; box-shadow:0 0 0 1px #C4A96A55; }

/* M1.8 social spine A2 — PvP-lite Duels results screen (app.js's playDuelResult).
   MIRRORS #warnedPanel's own minimal full-screen-panel shape (own id, own -inner
   wrapper, same z-index:100 tier as every other body-appended overlay in this file —
   #reveal/#splash/#worldcard/#warnedPanel) rather than #reveal's own elaborate staged
   choreography (a duel is a one-shot moment, no loot cascade to stagger). `.banner`/
   `.win`/`.loss` (styles.css, ~line 306/477) are reused VERBATIM, unscoped from
   #reveal's own id — see app.js's buildDuelResultHtml comment. */
#duelResult { position:fixed; inset:0; z-index:100; background:#08090C; color:#C3B48E;
              display:flex; align-items:center; justify-content:center; padding:24px;
              overflow-y:auto; }
#duelResult .reveal-inner { width:100%; max-width:360px; text-align:center; }
.duelflavor { color:#C3B48E; font-size:14px; margin-bottom:10px; }
.duellog { color:#A99A70; font-size:12.5px; font-style:italic; margin-bottom:6px; }
.duelrating { display:flex; align-items:center; justify-content:center; gap:8px;
              margin:16px 0 20px; font-size:14px; }
.duelratinglabel { color:#83764F; letter-spacing:.1em; font-size:11px; }
.duelratingval { color:#E6D5A8; font-weight:700; }
.duelratingdelta { font-weight:800; }
.duelratingdelta.up { color:#E6D5A8; }
.duelratingdelta.down { color:#C4553E; }
.duelratingdelta.even { color:#83764F; }

/* M1.9 Wave A1 B3 — feat/claim reward reveal (app.js's playClaimReward). MIRRORS
   #duelResult's minimal full-screen-panel shape above (own id, own -inner wrapper,
   z-index:100 like every other body-appended overlay — #reveal/#duelResult/#warnedPanel),
   so an item-reward feat claim gets the STANDARD loot grammar: a `.banner` + a global
   `.frcard.play.r-<rarity>` rarity bloom (both unscoped, reused verbatim) instead of a
   flat toast. */
#claimReward { position:fixed; inset:0; z-index:100; background:#08090C; color:#C3B48E;
               display:flex; align-items:center; justify-content:center; padding:24px;
               overflow-y:auto; }
#claimReward .reveal-inner { width:100%; max-width:360px; text-align:center; }
#claimReward .frcard { text-align:left; margin:14px 0 20px; }

/* M1.8 social spine A4 — the profile's state-aware ally + bounty controls
   (app.js's buildProfileHtml). A row of secondary (.btn2) actions under the primary
   red CHALLENGE (.bigbtn), so the flex-secondary verbs never compete with it visually. */
.profileactions { display:flex; flex-direction:column; gap:8px; margin-top:10px; }
.profileactions .btn2 { width:100%; padding:9px 0; font-size:12.5px; }

/* M1.8 social spine A5 — feed filter tabs [ World | Allies | Me ] (app.js's
   feedFilterTabsHtml). A slim segmented control between the feed header and the list,
   reusing .btn2's palette; the active scope reads as filled, the others as quiet. */
.feedfilters { display:flex; gap:6px; margin:8px 0 4px; }
.feedfilter { flex:1; background:#191D24; border:1px solid #353C46; color:#83764F;
              border-radius:2px; padding:6px 0; font-size:11.5px; font-weight:600;
              letter-spacing:.04em; cursor:pointer; }
.feedfilter.on { background:#212730; border-color:#353C46; color:#E6D5A8; }

/* M1.8 social spine A3 — Bounty Board + Allies hub (app.js's renderBounty). The board
   rows reuse the feed row's card language (.feeditem, above); the WANTED tag and the
   claim payout carry the same red/gold accents as combat/economy elsewhere. */
.bountyheader { display:flex; align-items:center; justify-content:space-between; gap:8px; }
.bountyrefresh { width:auto; padding:6px 12px; font-size:11px; }
.bountysub { color:#83764F; font-size:11.5px; margin:6px 0 10px; line-height:1.5; }
.bountylist { margin-top:4px; }
.bountyrow { background:#0E1014; border:1px solid #262C36; border-radius:2px;
             padding:9px 12px; margin-top:6px; display:flex; align-items:center;
             gap:10px; flex-wrap:wrap; font-size:12.5px; color:#C3B48E; }
.bountytag { background:#2a1414; border:1px solid #C4553E44; color:#C4553E;
             border-radius:2px; padding:1px 6px; font-size:10px; font-weight:800;
             letter-spacing:.08em; margin-right:5px; }
.bountytarget { flex:1; min-width:120px; color:#E6D5A8; font-weight:600; }
.bountyamt { color:#C3B48E; font-weight:700; white-space:nowrap; }
.bountypayout { color:#E6D5A8; font-size:11px; white-space:nowrap; }
.bountyexpiry { color:#83764F; font-size:10.5px; white-space:nowrap; }
.bountyposter { color:#6A6E77; font-size:10.5px; width:100%; }
/* D022 early-cancel control on the poster's own rows — subordinate (.btn2 palette) and
   compact, never competing with the board's primary read. */
.bountycancel { width:auto; padding:5px 12px; font-size:11px; }

/* Allies roster + incoming requests below the board. Chips reuse the app's chip
   language; a pending (incoming) chip glows violet as a "needs your action" cue. */
.allysection { margin-top:22px; }
.allyincoming { margin-bottom:12px; }
.allylist { display:flex; flex-wrap:wrap; gap:8px; margin-top:6px; }
.allychip { background:#0E1014; border:1px solid #262C36; border-radius:2px;
            padding:6px 10px; font-size:12px; color:#C3B48E; }
.allychip.pending { border-color:#2E343E; background:#0F1319; }
.allytag { color:#262C36; font-size:10px; margin-left:6px; }
.stub2 { color:#6A6E77; font-size:12px; font-style:italic; }

/* M1.9 item 22 — ARENA (PvP institution, DECISIONS D033). Reuses the app's dark palette +
   .sectionlabel/.stub2/.bigbtn/.btn2 conventions. The ENTER/LEAVE control is the primary
   consent verb (red .bigbtn weight); the ladder rows mirror .bountyrow chrome. */
.arena-headline { display:flex; align-items:center; justify-content:space-between; gap:8px; margin-top:6px; }
.arena-refresh { width:auto; padding:6px 12px; font-size:11px; }
.arena-status { font-size:12.5px; line-height:1.5; margin:8px 0; padding:8px 12px;
                border-radius:2px; border:1px solid #262C36; background:#0E1014; color:#C3B48E; }
.arena-status.in { border-color:#C4553E44; background:#1a1114; }
.arena-status.out { border-color:#2f6b3a55; background:#101a12; }
.arena-enter { background:linear-gradient(180deg,#171B22,#0E1014); color:var(--ember); }
.arena-leave { background:linear-gradient(180deg,#212730,#1F242C); }
.arena-record { margin-top:22px; }
.arena-recgrid { display:flex; flex-wrap:wrap; gap:10px 22px; font-size:13px; color:#E6D5A8; }
.arena-recgrid small { color:#83764F; }
.arena-rk { color:#83764F; font-size:11px; letter-spacing:.06em; margin-right:5px; }
.arena-prizeline { margin-top:10px; font-size:12px; color:#83764F; line-height:1.5; }
.arena-prizeline.ready { color:#C3B48E; }
.arena-prizeline.claimed { color:#E6D5A8; }
.arena-prize { background:linear-gradient(180deg,#262C36,#262C36); box-shadow:0 0 14px #C3B48E33; }
.arena-ladder { margin-top:24px; }
.arena-ladlist { margin-top:4px; }
.arena-ladrow { background:#0E1014; border:1px solid #262C36; border-radius:2px;
                padding:8px 12px; margin-top:6px; display:flex; align-items:center;
                gap:12px; font-size:12.5px; color:#C3B48E; }
.arena-ladrow.arena-selfrow { border-color:#212730; background:#12151B; }
.arena-rank { color:#83764F; font-weight:700; width:34px; }
.arena-lname { flex:1; min-width:120px; color:#E6D5A8; font-weight:600; }
.arena-here { color:#C4553E; font-size:11px; }
.arena-llvl { color:#83764F; font-size:11px; white-space:nowrap; }
.arena-lrating { color:#C3B48E; font-weight:700; white-space:nowrap; }
.arena-teams { margin-top:24px; opacity:.75; }

/* M1.8 social spine A3 — post-bounty modal (app.js's buildBountyModalHtml). Reuses
   #itemModal/.modal-backdrop/.modal-card; only the amount input + hint copy are new. */
.bountymodal { padding:6px 4px; }
.bountymodal-target { text-align:center; color:#C3B48E; font-size:14px; margin:6px 0 14px; }
.bountymodal-target b { color:#E6D5A8; }
.bountyinput { width:100%; box-sizing:border-box; background:#08090C; border:1px solid #353C46;
               border-radius:2px; color:#E6D5A8; padding:11px 12px; font-size:15px; text-align:center; }
.bountymodal-hint { color:#83764F; font-size:11px; line-height:1.5; margin:10px 0 14px; text-align:center; }

/* ---- Task 3 (Anticipation Gradient B client): warned-hunt interstitial ----------------
   docs/superpowers/specs/anticipation-gradient.md — a pre-hunt CHOICE (FIGHT/Slip),
   MIRRORING the encounter hook's own prompt+2-button shape (.encounter-hook/.enclabel/
   .encbtns/.encaccept/.encdecline above) but as its OWN full-screen body-appended panel
   (app.js's showWarnedInterstitial), not embedded in a results reveal — a warned offer
   exists BEFORE any hunt has resolved, so there is no #reveal to embed it in yet. Same
   "own id, torn down by remove()" convention #reveal/#splash/#worldcard already use;
   z-index:100 matches #reveal's own (the two are mutually exclusive in time — a warned
   offer is always resolved, one way or the other, before any reveal can exist). */
#warnedPanel { position:fixed; inset:0; z-index:100; background:#08090C; color:#C3B48E;
               display:flex; align-items:center; justify-content:center; padding:24px;
               overflow-y:auto; }
.warned-inner { width:100%; max-width:340px; text-align:center; background:#0F1319;
                border:1px solid #2E343E; border-radius:3px; padding:26px 20px; }
.warnlabel { color:#C9B98E; font-size:15px; font-style:italic; line-height:1.5; margin-bottom:18px; }
/* M1.9 B6 (declined-warned cooldown): the acknowledged re-offer copy ("You slipped
   away. The tracks remain…") reads a touch cooler/dimmer than a first-time warning —
   it's a callback, not a fresh alarm. */
.warnlabel.warned-acknowledged { color:#A99A70; }
.warnbtns { display:flex; flex-direction:column; gap:10px; }
/* FIGHT reuses .bigbtn (same visual weight as HUNT/HUNT AGAIN — a genuine escalation)
   with the encounter hook's own violet accept treatment (.encaccept); Slip mirrors
   .encdecline's plain bordered-quiet secondary. */
.warnfight { background:linear-gradient(180deg,#171B22,#0E1014); color:var(--ember); box-shadow:var(--ember-glow); }
.warnslip { width:100%; background:transparent; border:1px solid #262C36; border-radius:2px;
            color:#A99A70; padding:10px 0; font-weight:600; font-size:12.5px; }
.warnslip:active { background:#191D24; }

/* ==== M1.9 Wave C1 — the LEGIBILITY DOCTRINE: universal tooltip + materials + heat label
   (DECISIONS D028) ========================================================================
   web/app.js's universal tooltip component (the one #gametip node, delegated show/hide on
   hover/focus/long-press). z-index sits ABOVE #screenflash(150)/#reveal(100)/modals(90) so
   a tip reads over ANY surface it's triggered from. pointer-events:none so it never eats a
   click meant for the value under it. Server-sourced text only (every number computed
   server-side, GET /api/state's `tooltips` block) — this file only styles the container. */
.gametip { position:fixed; z-index:300; max-width:250px; pointer-events:none;
           background:#0c1020; color:#DDCB9E; border:1px solid #2E343E; border-radius:2px;
           padding:8px 11px; font-size:12px; line-height:1.45; box-shadow:0 6px 22px #000a;
           opacity:0; transition:opacity .12s; left:12px; top:12px; }
.gametip.show { opacity:1; }
/* A subtle affordance that a value is explainable — a help cursor on desktop, and (for the
   div-based cells) a dotted underline hint via a faint outline on keyboard focus. */
[data-tip] { cursor:help; }
[data-tip]:focus-visible { outline:2px solid #262C36; outline-offset:2px; border-radius:2px; }

/* ---- Materials panel (Character screen). Mirrors the .stats cell look so materials read
   as a peer of the stat/inventory grid. Each tile carries a what-it's-for/source tooltip;
   mining ore (bog_ore) — which persisted in material_stacks but rendered NOWHERE before —
   finally gets a home here (the M1.9 audit fix). */
.mats { display:grid; grid-template-columns:repeat(3, 1fr); gap:8px; margin:6px 0 14px; }
.mattile { background:#12151B; border:1px solid #212730; border-radius:2px; padding:9px 8px;
           display:flex; flex-direction:column; align-items:center; gap:3px; text-align:center; }
.mattile .matico { font-size:17px; }
.mattile .matqty { font-weight:700; color:#E6D5A8; font-size:14px; }
.mattile .matname { font-size:10.5px; letter-spacing:.03em; color:#A2946C; text-transform:uppercase; }
.mattile.empty { grid-column:1 / -1; flex-direction:row; justify-content:center; color:#83764F;
                 font-size:11.5px; text-transform:none; letter-spacing:0; cursor:default; }
/* M1.9 item 20 (D032): a deep-vein RARE-ORE tile is tinted by its own rarity. --mc is the
   ore's server-sent rarity color (build_material_meta) — the palette lives in data, never a
   client-side rarity->color mirror (CLAUDE.md hard rule 3). */
.mattile[data-rarity] { border-color: var(--mc); background:#161d2e; }
.mattile[data-rarity] .matqty { color: var(--mc); }
.mattile[data-rarity] .matname { color: var(--mc); }
/* The open-vein rare-strike card reuses the results glow (.frcard.play.r-<rarity>); this
   only adds a little breathing room above/below the beat. */
.frcard.mine-rare { margin:8px 0; }

/* ---- Heat meter label row (M1.9 Wave C1): the meter is no longer an unlabeled bar. A
   named header ("Fen Heat") + value sits above the fill; the whole .zoneheat binds the
   server explanation tooltip. */
.zoneheatrow { display:flex; justify-content:space-between; align-items:baseline; margin-bottom:3px; }
.zoneheatlabel { font-size:10px; letter-spacing:.05em; text-transform:uppercase; color:#c98f6b; font-weight:600; }
.zoneheatval { font-size:10px; color:#A2946C; font-variant-numeric:tabular-nums; }

/* COACH MARKS v1 (Post-Pass-7 #14, D044): one floating bubble at a time — fixed low
   position, pointer-events on itself only (never blocks the screen), tap-dismiss.
   The '?' re-summon sits quietly in the corner, above content but below overlays. */
.coachmark-bubble { position:fixed; left:50%; bottom:76px; transform:translateX(-50%);
  z-index:180; max-width:320px; background:#181C24; border:1px solid #262C3688;
  border-radius:3px; padding:12px 14px; cursor:pointer;
  box-shadow:0 8px 24px #00000088, 0 0 18px #262C3633; }
.coachmark-bubble .coachtext { color:#DDCB9E; font-size:13px; line-height:1.5; }
.coachmark-bubble .coachdismiss { color:#6A6E77; font-size:10.5px; margin-top:6px;
  text-align:right; letter-spacing:.04em; }
.coachhelp { position:fixed; right:14px; bottom:76px; z-index:60; width:28px; height:28px;
  border-radius:50%; background:#181C24; border:1px solid #353C46; color:#83764F;
  font-size:14px; cursor:pointer; opacity:.6; }
.coachhelp:hover { opacity:1; color:#C3B48E; }

/* POST-PASS-7 P0-7 (D044): the auto-banked-chain notice on the hunt reveal — a quiet
   reassurance ("you kept the pot") in the same green the loot uses, distinct from the
   at-risk amber. Sits between the banner and the new hunt's own loot. */
.rvbanked { color:#E6D5A8; font-size:12.5px; text-align:center; margin:4px 0 8px;
            padding:6px 10px; background:#0f1a12; border:1px solid #23402b; border-radius:2px; }

/* ART (Post-Pass-7 art pass): per-base weapon icon <img> (itemIcon). Sized to sit where
   the slot emoji did — fills the tile / card icon box without disturbing layout. The
   emoji fallback keeps its own intrinsic size, so only mapped bases render this. */
.cell .baseicon { width:82%; height:82%; object-fit:contain; display:block;
                  filter:drop-shadow(0 2px 4px #000000aa); }
.ic2 .baseicon, .fric .baseicon { width:100%; height:100%; object-fit:contain;
                  filter:drop-shadow(0 2px 5px #000000aa); }

/* POST-PASS-7 (Feats CLAIM ALL): the primary bulk-claim button at the top of Feats;
   disabled "Nothing to claim" reads muted. The summary card lists every reward with
   its rarity styling (never a silent grant). */
.claimall { margin:0 0 14px; }
.claimall.dis { opacity:.5; background:#181C24; color:#6A6E77; cursor:default; text-align:center; padding:12px; border-radius:2px; }
.cassummary { display:flex; flex-direction:column; gap:6px; max-height:52vh; overflow-y:auto;
              margin:12px 0 16px; width:100%; max-width:340px; }
.casrow { display:flex; align-items:center; gap:8px; padding:8px 10px; background:#0E1014;
          border:1px solid #262C36; border-left-width:3px; border-radius:2px; font-size:13px; }
.casrow .casicon { width:22px; height:22px; display:inline-flex; align-items:center; justify-content:center; }
.casrow .casicon .baseicon { width:100%; height:100%; object-fit:contain; }
.casrow .casname { color:#E6D5A8; }
.casrow .trophylvl { color:#83764F; font-style:italic; margin-left:auto; }

/* W1-STATIC (D045): the Map — owned Blackmire claim-grounds. House banners across the
   top, a grid of claim cards each with holder sigil + hold-yield + a DORMANT control
   meter (rendered but static at W1; W2's contests move it). Art slot (.claimcard-art)
   reserved for the 512x320 claim-card splash — the pipeline drops a background-image in. */
.mapbanners { display:flex; flex-wrap:wrap; gap:8px; margin:6px 0 10px; }
.mapbanner { font-size:12.5px; padding:5px 10px; border-radius:20px; color:var(--hc);
             border:1px solid var(--hc); background:#0C0E12; }
.mapintro { color:#83764F; font-style:italic; font-family:var(--serif);
            font-size:13px; margin-bottom:14px; }
.claimgrid { display:grid; grid-template-columns:1fr; gap:12px; }
@media (min-width:720px) { .claimgrid { grid-template-columns:1fr 1fr; } }
.claimcard { background:#0E1014; border:1px solid #262C36; border-radius:3px;
             overflow:hidden; cursor:pointer; transition:border-color .15s; }
.claimcard:hover, .claimcard:focus { border-color:#262C3688; outline:none; }
.claimcard-art { height:110px; background:#0C0E12 center/cover no-repeat;
                 border-bottom:1px solid #262C36;
                 background-image:linear-gradient(135deg,#141d30,#0C0E12 70%); }
/* Claim-card art (delivered art/zone-<slug>-card.png for the 3 art-backed claims). No scrim
   needed — the claim text lives in .claimcard-body BELOW the art strip, never over it. A 404
   leaves the placeholder gradient above (rule 6). The other 3 claims keep the placeholder. */
/* 2026-07-17 ground-art drop: the per-key background rules moved into app.js's ONE
   CLAIM_ART map (conscious-coverage law, CI-parsed) — claimCardHtml inlines the card
   art; this base rule is the intentional fallback panel for null entries. */
.claimcard-body { padding:12px 14px; }
.claimcard-top { display:flex; justify-content:space-between; align-items:baseline; gap:8px; }
.claimname { font-size:15px; font-weight:600; color:#E6D5A8; }
.claimholder { font-size:11.5px; color:var(--hc); white-space:nowrap; }
.claimepigraph { color:#83764F; font-style:italic; font-family:var(--serif);
                 font-size:12px; margin:5px 0 8px; }
.claimyield { color:#E6D5A8; font-size:12px; margin-bottom:9px; }
.claimctrl { margin-top:4px; }
.claimctrl.dormant { opacity:.6; }
.claimctrlbar { height:5px; background:#08090C; border-radius:3px; overflow:hidden; border:1px solid #262C36; }
.claimctrlbar > div { height:100%; background:linear-gradient(90deg,#83764F,#83764F); }
.claimctrllabel { display:block; font-size:10px; color:#6A6E77; letter-spacing:.05em; margin-top:3px; }
/* Claim-page banner: a full-bleed header strip (bleeds past the .isheet 14px padding to the
   card edges, top corners rounded to match). No text over it — the claim name sits below. */
.claimpage .claimpage-banner { height:118px; margin:-14px -14px 12px; border-radius:2px 2px 0 0;
   overflow:hidden; background-size:cover; background-position:center; }
.claimpage .claimpage-holder { color:var(--hc); font-size:13px; margin:4px 0 8px; }
.claimpage .claimpage-row { font-size:13px; color:#C3B48E; margin:6px 0; }
.claimpage .cpk { color:#6A6E77; display:inline-block; min-width:92px; }

/* ============================================================================
   Type law (5a) — world-voice register. Spectral serif carries every NAME, TITLE
   and voiced NUMBER (Gear Score, stats, item power); the interface — tier badges,
   micro-labels, tabular counters, toggles — stays in the Plex-sans body default.
   Placed last so it wins font-family over each surface's own rule by source order;
   it sets ONLY font-family (+ a resting weight for the un-ruled reveal name), so no
   size/spacing changes here — the layout is unchanged, only the register. */
.charname, .zcard b, .zname, .iname, .nm2, .cmpcard .iname, .claimname, .customname,
.gearscore, .gearscore b, .rp-gearscore b, .stats b, .ipower, .mapname, .featname,
.bountyname, .arena-name, .sellpayout b, .lifetime-name, .duel-name, .rrname,
.splash-title, .worldcard-title, .basename, .frname, .craftname, .traderow .tname {
  font-family: var(--serif);
}
.nm2 { font-family: var(--serif); font-weight: 600; }
/* interface counters that must stay tabular sans even though they sit near serif
   numbers (the currency header + energy/heat readouts are counters, not voiced). */
#strip, #rightpanel .rp-wallet, .ewcountdown, .zoneheatval, .basechip {
  font-family: var(--sans);
}

/* Browser-native pass: the connection-lost banner (app.js renderOfflineBanner). Fixed at the
   top above everything; cold danger rust so it reads as transient state, never a rarity. */
.offline-banner { position:fixed; top:0; left:0; right:0; z-index:400; text-align:center;
  background:#0D1016; color:var(--danger); border-bottom:1px solid var(--danger);
  font-family:var(--sans); font-size:12.5px; letter-spacing:.04em; padding:7px 10px; }

/* Type fix (Post-Pass-8 P2): form controls do NOT inherit the body font — a bare <button>
   or <input> falls to the UA default (Arial), so the two most-clicked controls (HUNT/ENTER
   and the nav) were off-brand. Route them to the interface sans (the world-voice serif is
   applied explicitly by the type-law block above where a control needs it). */
button, input, select, textarea, #tabs button { font-family: var(--sans); }

/* --- A6: the "while you were away" return card (spec m1.8-social-spine.md) --------------
   Rides the shared modal grammar (.modal-backdrop/.modal-card, above) -- this only styles
   the card's own internals. Iron Sanctum discipline: cold iron panel, ember reserved for
   the CTA edge the bigbtn already owns; the per-kind dot is the ONLY color, and it is a
   BEAT tag (raid/bounty/rating/world/energy/news), never a rarity tint -- rarity hexes stay
   reserved for rarity (the audit's 33-surface reservation sweep). */
.awaycard { background:var(--panel, #0E1014); border:1px solid #262C36; border-radius:2px;
            padding:16px; }
.awayelapsed { font-family:var(--serif); font-style:italic; color:#83764F; font-size:12.5px;
               margin:-2px 0 12px; }
.awaylines { display:flex; flex-direction:column; gap:6px; margin-bottom:14px; }
/* Each beat is a real <button> (a doorway to the tab the SERVER assigned it), so it is
   keyboard-reachable and hit-targets like every other control -- not a div pretending. */
.awayline { display:flex; align-items:flex-start; gap:9px; width:100%; text-align:left;
            background:#0C0E12; border:1px solid #1F242C; border-radius:2px;
            padding:9px 10px; color:#C3B48E; font-size:13px; line-height:1.35;
            font-family:var(--sans); cursor:pointer; }
.awayline:hover { background:#12161C; border-color:#353C46; }
.awaykind { width:6px; height:6px; border-radius:50%; margin-top:5px; flex:0 0 auto;
            background:#6A6E77; }
.awaytext { flex:1; }
/* Beat tags. Ember for the two that are ABOUT you being hunted (the card's whole reason to
   exist); everything else stays quiet iron/ash so the raid line reads first at a glance. */
.awaykind.k-raid   { background:#E7A83E; box-shadow:0 0 6px #E7A83E66; }
.awaykind.k-bounty { background:#E7A83E; }
.awaykind.k-rating { background:#8A6A2E; }
.awaykind.k-world  { background:#5A7A8A; }
.awaykind.k-energy { background:#83764F; }
.awaykind.k-quiet  { background:#3A3F47; }
