/* ui/tapestry/tapestry.css — THE TAPESTRY compendium screen. (#173 build wave 3)
   Extends src/styles.css + ui/loadout/loadout.css tokens (--green/--amber/--void/--green-dim/
   --rarity- / --elem- / --amber-dim families). Loaded once from index.html beside loadout.css.
   No external assets — procedural CSS only. THE WEAVE pass (tapestry-lore-spec.md) ships
   the woven-cloth backdrop (one-shot canvas tile painted in tapestry.js, applied as a
   repeating background on .tap-body), the lore-card grid, and the comic-splash reader.
   THE FOES pass (faction-card-spec.md) adds the dossier grid + the dossier sheet, which
   re-uses .lore-reader's full-screen chrome rather than growing a second overlay. */

.tapestry-panel {
  display: flex;
  flex-direction: column;
  height: 100%;
  overflow: hidden;
  color: var(--green);
}

/* ── Sub-tab bar (THE LOOP / CLASSES / ARSENAL / THE CHASE / THE WEAVE / THE FOES) ────────────── */
.tap-tabbar {
  display: flex;
  gap: 4px;
  padding: 8px 16px 0;
  border-bottom: 1px solid var(--green-dim);
  flex-shrink: 0;
  flex-wrap: wrap;
}
.tap-tab {
  background: none;
  border: none;
  border-bottom: 2px solid transparent;
  color: var(--green);
  opacity: .65;
  font-family: inherit;
  font-size: 11px;
  letter-spacing: 1.5px;
  padding: 8px 14px;
  cursor: pointer;
  min-height: 40px;
  text-transform: uppercase;
}
.tap-tab.active,
.tap-tab:hover { color: var(--amber); opacity: 1; border-bottom-color: var(--amber); }

.tap-body {
  flex: 1;
  overflow-y: auto;
  padding: 14px 16px 24px;
  display: flex;
  flex-direction: column;
  gap: 10px;
}

/* ── Shared text/heading primitives ─────────────────────────────────────── */
.tap-p { font-size: 11px; line-height: 1.5; opacity: .85; margin: 0; text-transform: none; }
.tap-p-dim { opacity: .6; font-style: italic; }
.tap-section-head {
  color: var(--amber);
  font-size: 12px;
  letter-spacing: 2.5px;
  margin-top: 10px;
  padding-top: 8px;
  /* Thread-line rule (wow-cohesion pass): a gold thread running out of the head,
     replacing the flat green hairline — reads as a strand of the tapestry. */
  background: linear-gradient(90deg, var(--amber) 0, rgba(255, 176, 0, .28) 42%, transparent 88%) left top / 100% 1px no-repeat;
}
.tap-subgroup-head {
  color: var(--green);
  opacity: .7;
  font-size: 10px;
  letter-spacing: 2px;
  margin-top: 4px;
}

/* ── Iconography (procedural sigils — ui/shared/element-icon.js + ability-icon.js +
      loadout/icons.js). Break up the text; one visual system across every surface. ──
   Section-lead cards (element/reaction/class/variant/ability/archetype/ladder/exotic —
   D1, menus-refit-spec.md) size their icon via the shared .tap-hero-icon frame below,
   not a per-context class. These sizes remain for the inline/small uses that stay
   small: THE LOOP's diagram-step + Fate-meter heading glyphs.
   (CHASE's named-table entries were in this list; playtest 2026-07-25 promoted them
   to the shared .tap-hero-icon frame — they're the cards you land on after a run, so
   they read as collection entries now, not leaf detail. .tap-named-icon only tunes
   the gear sigil's aspect INSIDE that frame — gun sigils are wide, not square.) */
.tap-diagram-icon { width: 22px; height: 22px; flex: 0 0 auto; }
.tap-head-icon { width: 18px; height: 18px; flex: 0 0 auto; vertical-align: -4px; margin-right: 5px; }
.tap-section-head-ic { display: flex; align-items: center; }
.tap-hero-icon > svg.tap-named-icon { width: 76%; height: auto; }

/* ── Tab hero banners (the lorebook-splash treatment, tapestry.js _tabHero) ──
   Every sub-page opens on a full-width splash: one-shot paintSplash canvas
   (cosmos + Kirby ray burst + halftone floor + ink frame) behind a large
   procedural portrait, a Kirby title slab, and a green kicker line saying what
   the tab is FOR. CLASSES uses the compact variant per class card instead. */
.tap-tabhero {
  --tabhero-accent: var(--amber);
  position: relative;
  overflow: hidden;
  border-radius: 3px;
  min-height: clamp(170px, 26vh, 250px);
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 8px;
  padding: 18px 16px 22px;
  margin-bottom: 8px;
  flex-shrink: 0;
}
.tap-tabhero-splash { position: absolute; inset: 0; width: 100%; height: 100%; }
.tap-tabhero-portrait {
  position: relative;
  width: clamp(72px, 13vh, 116px);
  aspect-ratio: 1;
  display: grid;
  place-items: center;
  filter: drop-shadow(0 0 16px color-mix(in srgb, var(--tabhero-accent) 45%, transparent));
}
.tap-tabhero-portrait > svg { width: 100%; height: 100%; color: var(--tabhero-accent); }
/* Kirby title slab — mirrors .lr-title's offset-print drop (our own class;
   tapestry.css never reaches into the reader's selectors). */
.tap-tabhero-title {
  position: relative;
  font-size: clamp(20px, 4.5vw, 36px);
  letter-spacing: 4px;
  font-weight: 700;
  text-align: center;
  text-transform: uppercase;
  line-height: 1.05;
  color: var(--tabhero-accent);
  text-shadow: 3px 3px 0 rgba(0, 0, 0, .9), -1px -1px 0 rgba(255, 255, 255, .12);
}
.tap-tabhero-kicker {
  position: relative;
  font-size: 10.5px;
  letter-spacing: 2px;
  font-style: italic;
  text-align: center;
  text-transform: none;
  color: var(--green);
  opacity: .85;
  max-width: 620px;
  line-height: 1.5;
  text-shadow: 0 1px 0 rgba(0, 0, 0, .8);
}
.tap-tabhero-badge { position: absolute; top: 8px; right: 10px; z-index: 1; }
/* CLASSES — compact per-class hero, bled to the card's edges so the splash IS
   the card's masthead (offsets match .tap-card's 10px/12px padding). */
.tap-tabhero-class {
  min-height: clamp(130px, 19vh, 190px);
  margin: -10px -12px 4px;
  border-radius: 2px 2px 0 0;
}

/* ── Hero-scale icon frame (D1) — one visual grammar for every section-lead card.
   Mirrors ui/loadout/loadout.css .lo-eq-classhero-icon's grammar (framed accent
   box, radial-gradient backdrop, glow-tinted child) as OUR OWN class (tapestry.css
   never references loadout.css selectors). Works for an SVG icon child (element/
   reaction/ability/gear/rarity sigils, sized via > svg) or a bare text glyph (the
   class planet glyph — .tap-hero-icon-glyph, no SVG factory exists for it). */
.tap-hero-icon {
  --hero-accent: var(--amber);
  width: clamp(64px, 9vw, 96px);
  aspect-ratio: 1;
  flex-shrink: 0;
  border: 2px solid var(--hero-accent);
  border-radius: 10px;
  background:
    radial-gradient(120% 100% at 30% 20%, color-mix(in srgb, var(--hero-accent) 26%, transparent), rgba(5, 9, 22, .92) 78%);
  box-shadow: inset 0 0 0 3px rgba(6, 10, 26, .85), inset 0 0 18px rgba(0, 0, 0, .5);
  display: grid;
  place-items: center;
}
.tap-hero-icon > svg {
  width: 66%;
  height: 66%;
  filter:
    drop-shadow(0 0 8px color-mix(in srgb, var(--hero-accent) 55%, transparent))
    drop-shadow(1.5px 2px 0 rgba(0, 0, 0, .5));
}
.tap-hero-icon-glyph {
  font-size: clamp(32px, 5vw, 48px);
  line-height: 1;
  color: var(--hero-accent);
  text-shadow: 0 0 10px color-mix(in srgb, var(--hero-accent) 55%, transparent);
}
/* Icon-left / text-right row (D1 "the icon carries the row") — the shared layout
   every section-lead card adopts around its .tap-hero-icon. */
.tap-hero-row { display: flex; align-items: flex-start; gap: 12px; }
.tap-hero-row > .tap-hero-body { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; gap: 4px; }

/* ── THE LOOP ────────────────────────────────────────────────────────────── */
.tap-diagram { display: flex; align-items: stretch; gap: 8px; flex-wrap: wrap; }
.tap-diagram-title { display: flex; align-items: center; gap: 6px; }
.tap-diagram-step {
  flex: 1 1 200px;
  border: 1px solid var(--green-dim);
  border-radius: 2px;
  padding: 10px 12px;
  background: rgba(51, 255, 102, .03);
}
.tap-diagram-title { color: var(--amber); font-size: 11px; letter-spacing: 1.5px; margin-bottom: 4px; }
.tap-diagram-body { font-size: 10px; line-height: 1.4; opacity: .8; text-transform: none; }
.tap-diagram-arrow {
  display: flex; align-items: center; justify-content: center;
  color: var(--amber); font-size: 18px; opacity: .6; flex: 0 0 auto; padding: 0 2px;
}

/* Element + reaction cards (D1) — icon-left/text-right hero rows (.tap-hero-row,
   applied alongside these classes in tapestry.js) inside a bordered card shell. */
.tap-elem-grid, .tap-react-grid, .tap-det-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 8px; }
.tap-elem-card, .tap-react-card, .tap-det-card {
  border: 1px solid var(--green-dim);
  border-left: 3px solid var(--elem-color, var(--green));
  border-radius: 2px;
  padding: 10px 12px;
  background: rgba(255, 255, 255, .02);
}
.tap-elem-name { color: var(--elem-color, var(--amber)); font-size: 13px; letter-spacing: 1.5px; }
.tap-elem-blurb { font-size: 10px; opacity: .8; line-height: 1.4; text-transform: none; }

.tap-react-name { color: var(--amber); font-size: 13px; letter-spacing: 1.5px; }
.tap-react-pair { font-size: 9px; opacity: .6; margin: 2px 0; }
.tap-react-lean { font-size: 9px; color: var(--sort-accent, #4fd6ff); letter-spacing: 1px; margin-bottom: 4px; }
.tap-react-desc { font-size: 10px; opacity: .8; line-height: 1.4; text-transform: none; }

.tap-fate-grid { display: flex; flex-direction: column; gap: 3px; }
.tap-fate-row { display: flex; justify-content: space-between; gap: 10px; font-size: 10px; border-bottom: 1px dotted var(--green-dim); padding: 3px 0; }
.tap-fate-k { opacity: .7; }
.tap-fate-v { color: var(--amber); }

/* ── Cards (shared: CLASSES / ARSENAL / CHASE exotics) ──────────────────── */
.tap-card {
  border: 1px solid var(--green-dim);
  border-radius: 2px;
  padding: 10px 12px;
  background: rgba(255, 255, 255, .02);
  display: flex;
  flex-direction: column;
  gap: 6px;
}
.tap-card-head { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.tap-card-name { color: var(--amber); font-size: 12px; letter-spacing: 1.5px; flex: 1; }
.tap-card-lore { font-size: 10px; opacity: .6; font-style: italic; text-transform: none; }

.tap-chassis-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 4px; }
.tap-chassis-row { display: flex; justify-content: space-between; font-size: 10px; gap: 6px; }
.tap-chassis-k { opacity: .6; }
.tap-chassis-v { color: var(--green); }

.tap-variant-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 8px; }
.tap-variant-card { border: 1px solid var(--green-dim); border-radius: 2px; padding: 8px 10px; }
.tap-variant-label { color: var(--amber); font-size: 10.5px; letter-spacing: 1px; }
.tap-variant-desc { font-size: 9.5px; opacity: .8; line-height: 1.35; text-transform: none; }

/* ── CLASSES: free-slot ability catalog ─────────────────────────────────── */
.tap-ability-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 8px; margin-bottom: 6px; }
.tap-ability-card { border: 1px solid var(--green-dim); border-radius: 2px; padding: 8px 10px; background: rgba(255, 255, 255, .02); }
.tap-ability-name { color: var(--amber); font-size: 10.5px; letter-spacing: 1px; }
.tap-ability-meta { font-size: 8.5px; opacity: .6; letter-spacing: .5px; margin: 3px 0; }
.tap-ability-desc { font-size: 9px; opacity: .8; line-height: 1.35; text-transform: none; }

/* ── ARSENAL ─────────────────────────────────────────────────────────────── */
.tap-slot-tag {
  font-size: 9px; letter-spacing: 1px; padding: 2px 6px;
  border: 1px solid var(--green-dim); border-radius: 2px; opacity: .8;
}
.tap-source-tag { font-size: 9px; opacity: .6; letter-spacing: .5px; }
.tap-starter-row { font-size: 10px; }
.tap-starter-label { color: var(--amber); opacity: .8; }
.tap-starter-name { color: var(--amber); }
.tap-starter-perks { opacity: .75; text-transform: none; }

/* Playtest 2026-07-25 — the perk catalog under each shape was 9 columns wide at
   desktop, which squeezed every description into a 3–4 line sliver. Wider minimum
   → ~5-up at 1600px, ~4-up at 1280px, with type big enough to actually read. */
.tap-perk-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 7px; }
.tap-perk-chip { border: 1px solid var(--green-dim); border-radius: 2px; padding: 7px 9px; background: rgba(255, 255, 255, .02); }
.tap-perk-chip-name { color: var(--green); font-size: 11px; letter-spacing: .5px; }
.tap-perk-chip-desc { font-size: 10px; opacity: .75; line-height: 1.35; margin-top: 3px; text-transform: none; }
/* §9.5 — a perk rolled onto any owned item of this archetype. Deliberately
   subtle (a tick + border tint, not a state badge) — chips are dense, and
   this must not compete with the card's own ownership read. */
.tap-perk-chip-rolled { border-color: var(--amber); }
.tap-perk-chip-rolled .tap-perk-chip-name::before { content: '✓ '; color: var(--amber); }
/* The archetype hero art is the ARSENAL card's anchor — a notch above the shared
   section-lead frame so the gun shape reads before the text does. */
.tap-arch-icon { width: clamp(76px, 10vw, 116px); }
.tap-arsenal .tap-card-name { font-size: 15px; }

/* NAMED GUNS row (R3 call 24) — the archetype card's click-through to the
   catalog's actual guns of that shape. `.tap-state-*` (below) already carries
   the border/opacity story; this only adds the row layout + the two text lines. */
.tap-named-gun-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 6px; margin-top: 4px; }
.tap-named-gun-btn {
  font-family: inherit;
  display: flex; flex-wrap: wrap; align-items: baseline; gap: 6px;
  text-align: left;
  border: 1px solid var(--green-dim); border-radius: 2px;
  padding: 6px 9px;
  background: rgba(255, 255, 255, .02);
  cursor: pointer;
  min-height: 32px;
}
.tap-named-gun-btn:hover { background: rgba(255, 255, 255, .06); border-color: var(--green); }
.tap-named-gun-btn:disabled { cursor: default; opacity: .55; }
.tap-named-gun-btn:disabled:hover { background: rgba(255, 255, 255, .02); border-color: var(--green-dim); }
.tap-named-gun-name { color: var(--green); font-size: 10.5px; letter-spacing: .5px; flex: 1 1 auto; }
.tap-named-gun-meta { font-size: 8.5px; opacity: .65; letter-spacing: .5px; }
.tap-named-gun-btn .tap-badge { font-size: 8px; padding: 1px 5px; }
.tap-ladder-tag { float: right; font-size: 7.5px; letter-spacing: 1px; padding: 1px 4px; border-radius: 2px; }
.tap-ladder-tag.tap-ladder-safe { color: var(--rarity-rare, #4db5ff); border: 1px solid var(--rarity-rare, #4db5ff); }
.tap-ladder-tag.tap-ladder-deep { color: var(--rarity-legendary, #c08cff); border: 1px solid var(--rarity-legendary, #c08cff); }

/* ── THE CHASE ───────────────────────────────────────────────────────────── */
.tap-ladder-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(230px, 1fr)); gap: 8px; }
.tap-ladder-card {
  border: 1px solid var(--green-dim);
  border-top: 3px solid var(--rarity-color, var(--green));
  border-radius: 2px;
  padding: 10px 12px;
  background: rgba(255, 255, 255, .02);
}
.tap-ladder-card.tap-ladder-woven { box-shadow: 0 0 8px rgba(255, 215, 106, .25) inset; }
.tap-ladder-name { color: var(--rarity-color, var(--amber)); font-size: 12px; letter-spacing: 1.5px; }
.tap-ladder-promise { font-size: 9px; opacity: .7; letter-spacing: .5px; margin: 3px 0; }
.tap-ladder-desc { font-size: 9.5px; opacity: .8; line-height: 1.4; text-transform: none; }

/* ── MINISTRY SETS — the armor-chase strip (cosmetics-spec.md §10 Phase B;
   §12 Q3: THE CHASE hosts the per-set completion row + SET star). Same 3-up
   as the named tables — exactly three ministries, one row each. */
.tap-sets-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 10px; }
.tap-set-row {
  border: 1px solid var(--green-dim);
  border-radius: 2px;
  padding: 10px 12px;
  display: flex;
  flex-direction: column;
  gap: 4px;
}
.tap-set-name { color: var(--amber); font-size: 12px; letter-spacing: 1.5px; flex: 1; }
.tap-set-read { font-size: 9.5px; opacity: .65; letter-spacing: .5px; }
.tap-set-row .tap-badge { align-self: flex-start; margin-top: 3px; font-size: 9px; padding: 2px 7px; }

.tap-source-card { border-top: 1px dotted var(--green-dim); padding-top: 6px; margin-top: 4px; }
/* §9.1/§9.2 — the header is now a row: label, the per-table WOVEN counter, and
   (pre-run only) the RUN THIS STRIKE launch button. */
.tap-source-head { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin-bottom: 5px; }
.tap-source-label { color: var(--amber); font-size: 10.5px; letter-spacing: 1.5px; flex: 1 1 auto; }
.tap-source-launch {
  font-family: inherit;
  font-size: 9px;
  letter-spacing: 1px;
  padding: 4px 9px;
  border-radius: 2px;
  cursor: pointer;
  white-space: nowrap;
  background: rgba(255, 176, 0, .08);
  color: var(--amber);
  border: 1px solid var(--amber-dim, rgba(255, 176, 0, .3));
}
.tap-source-launch:hover { background: rgba(255, 176, 0, .18); border-color: var(--amber); }
/* Named loot tables — a fixed 3-up (playtest 2026-07-25). A loot table holds exactly
   three entries (two weapons + one armor), so hard-coding three columns keeps one
   table on one row at any desktop width instead of letting auto-fit reflow it into a
   ragged 4-up with a hole. eff-w-le-900 below collapses it to a single column. */
.tap-named-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 10px; }
.tap-named-card {
  border: 1px solid var(--green-dim);
  border-radius: 2px;
  padding: 10px 12px;
  background: rgba(255, 255, 255, .02);
}
.tap-named-head { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
.tap-named-name { color: var(--green); font-size: 13px; letter-spacing: 1px; flex: 1; text-transform: none; }
.tap-named-meta { font-size: 9.5px; opacity: .65; letter-spacing: .5px; }
/* The scannable roll line (weapon-perk-matrix-spec §13.4 concept) — what the
   card's curated right pool offers, on the card face. Amber like the perk
   ladder tags it echoes, never competing with the name for the eye. */
.tap-named-roll { font-size: 9px; opacity: .8; letter-spacing: .5px; color: var(--amber); margin-top: 1px; text-transform: none; }
/* The state badge is the card's payload — its own line, left-aligned (the flex-column
   .tap-hero-body would otherwise stretch it edge to edge). */
.tap-named-card .tap-badge { align-self: flex-start; margin-top: 3px; font-size: 9px; padding: 2px 7px; }
/* Named entries sit one tier below the section-lead cards, so their hero frame is a
   notch smaller than the 64–96px default. */
.tap-named-card .tap-hero-icon { width: clamp(52px, 5vw, 68px); border-radius: 8px; }

.tap-exotic-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 8px; }
.tap-catalyst-row { font-size: 9.5px; }
.tap-catalyst-label { color: var(--rarity-exotic, #ffd34d); letter-spacing: .5px; }

/* ── Patch-on-cloth grammar (wow-cohesion pass, tapestry-lore-spec.md §6b) ──
   Every card across ALL five tabs is a PATCH stitched onto the woven-cloth
   backdrop: an opaque dark slab (so the cloth shows in the gutters, not through
   the card) + a faint dashed stitch inset. Matches .tap-lore-card's grammar. */
.tap-card,
.tap-elem-card,
.tap-react-card,
.tap-det-card,
.tap-ladder-card,
.tap-variant-card,
.tap-ability-card,
.tap-diagram-step,
.tap-named-card,
.tap-set-row,
.tap-attune {
  background: rgba(5, 9, 22, .74);
  outline: 1px dashed rgba(51, 255, 102, .13);
  outline-offset: -4px;
}
/* CLASSES — per-chassis accent (set as --class-accent in tapestry.js; matches the
   chassis lore-card accents): colored spine; the name/glyph live in the card's
   splash hero banner (.tap-tabhero-class), tinted by its own --tabhero-accent. */
.tap-class-card { border-left: 4px solid var(--class-accent, var(--amber)); }

/* ── Entry ownership / mastery states (§5 — border/badge styling ONLY, never gating) ── */
.tap-state-unowned { border-style: dashed; opacity: .78; }
.tap-state-owned { border-color: var(--green); }
.tap-state-mastered {
  border-color: var(--rarity-exotic, #ffd34d);
  box-shadow: 0 0 6px rgba(255, 211, 77, .25);
}
.tap-badge {
  font-size: 7.5px;
  letter-spacing: 1px;
  padding: 1px 5px;
  border-radius: 2px;
  white-space: nowrap;
}
.tap-badge-unowned { color: var(--green); opacity: .45; border: 1px dashed var(--green-dim); }
.tap-badge-owned { color: var(--green); border: 1px solid var(--green); }
.tap-badge-mastered { color: var(--rarity-exotic, #ffd34d); border: 1px solid var(--rarity-exotic, #ffd34d); }
/* §9.2 — completion counters (per-table "1/3 WOVEN" + the tab headline). A
   notch bigger than the base .tap-badge — this is a number the player reads
   at a glance, not a leaf-entry tag. */
.tap-woven-counter { font-size: 9px; padding: 2px 7px; }

/* ── "Earned this run" spotlight — the post-mission weave-in flash ──────────
   showEarned() (ui/tapestry/tapestry.js) adds .tap-earned to the card(s) matching
   what just dropped after opening THE CHASE / ARSENAL and scrolling here, so the
   player watches the tapestry fill in for what they did (tapestry-compendium.md §5).
   One-shot: the class is stripped after config.tapestry.WEAVE_IN_MS (fed in as the
   --tap-weave-ms inline prop) so a later refresh won't replay it. */
.tap-earned {
  position: relative;
  z-index: 1;
  /* duration comes from config via the --tap-weave-ms inline prop (showEarned) */
  animation: tap-weave-in var(--tap-weave-ms, 2400ms) ease-out 1;
}
.tap-earned::after {
  content: '✦ WOVEN THIS RUN';
  position: absolute;
  top: -8px;
  right: 8px;
  font-size: 7.5px;
  letter-spacing: 1px;
  padding: 1px 6px;
  border-radius: 2px;
  color: #1a1205;
  background: var(--rarity-exotic, #ffd34d);
  box-shadow: 0 0 8px rgba(255, 211, 77, .6);
  pointer-events: none;
  animation: tap-earned-ribbon var(--tap-weave-ms, 2400ms) ease-out 1;
}
@keyframes tap-weave-in {
  0%   { box-shadow: 0 0 0 0 rgba(255, 211, 77, 0); border-color: var(--rarity-exotic, #ffd34d); }
  12%  { box-shadow: 0 0 18px 4px rgba(255, 211, 77, .75); border-color: var(--rarity-exotic, #ffd34d); transform: scale(1.015); }
  60%  { box-shadow: 0 0 10px 1px rgba(255, 211, 77, .4); transform: scale(1); }
  100% { box-shadow: 0 0 0 0 rgba(255, 211, 77, 0); }
}
@keyframes tap-earned-ribbon {
  0%   { opacity: 0; transform: translateY(4px); }
  15%  { opacity: 1; transform: translateY(0); }
  82%  { opacity: 1; }
  100% { opacity: 0; }
}
@media (prefers-reduced-motion: reduce) {
  .tap-earned { animation: none; box-shadow: 0 0 10px 2px rgba(255, 211, 77, .5); }
  .tap-earned::after { animation: none; opacity: 1; }
}

/* ── The star (attunement affordance) ───────────────────────────────────── */
.tap-star {
  background: none;
  border: none;
  color: var(--green);
  opacity: .5;
  font-size: 15px;
  line-height: 1;
  cursor: pointer;
  padding: 2px 4px;
}
.tap-star:hover { opacity: .9; }
.tap-star-active { color: var(--amber); opacity: 1; text-shadow: 0 0 6px rgba(255, 176, 0, .5); }

/* ── Attunement bar (ARSENAL + THE CHASE) ───────────────────────────────── */
.tap-attune {
  border: 1px solid var(--amber-dim, rgba(255, 176, 0, .3));
  border-radius: 2px;
  padding: 10px 12px;
  background: rgba(255, 176, 0, .04);
  display: flex;
  flex-direction: column;
  gap: 6px;
  margin-bottom: 4px;
}
/* §9.3 — head row now carries the HIDE WOVEN toggle beside the bar's title;
   §9.4 — the odds note sits just under it, read live off config.loot.ATTUNE. */
.tap-attune-headrow { display: flex; align-items: center; justify-content: space-between; gap: 10px; flex-wrap: wrap; }
.tap-attune-head { color: var(--amber); font-size: 11px; letter-spacing: 2px; }
.tap-attune-odds { font-size: 9px; opacity: .65; letter-spacing: .3px; text-transform: none; }
.tap-attune-empty { font-size: 10px; opacity: .7; text-transform: none; }
.tap-hide-woven {
  font-family: inherit;
  font-size: 9px;
  letter-spacing: 1px;
  padding: 4px 9px;
  border-radius: 2px;
  cursor: pointer;
  white-space: nowrap;
  background: rgba(255, 255, 255, .04);
  color: var(--green);
  border: 1px solid var(--green-dim);
}
.tap-hide-woven:hover { border-color: var(--amber); color: var(--amber); }
.tap-hide-woven-active { color: var(--amber); border-color: var(--amber); background: rgba(255, 176, 0, .12); }
.tap-attune-row { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.tap-attune-label { color: var(--amber); font-size: 11px; letter-spacing: .5px; text-transform: none; }
.tap-attune-status { font-size: 9px; letter-spacing: 1px; padding: 2px 6px; border-radius: 2px; }
.tap-attune-status.tap-attune-funded { color: var(--green); border: 1px solid var(--green); }
.tap-attune-status.tap-attune-dormant { color: var(--red); opacity: .85; border: 1px solid var(--red); }
.tap-attune-runs {
  font-family: inherit;
  font-size: 10px;
  letter-spacing: .5px;
  background: var(--void);
  color: var(--green);
  border: 1px solid var(--green-dim);
  padding: 5px 6px;
  border-radius: 2px;
}
.tap-attune-cost { font-size: 10px; color: var(--amber); }
.tap-attune-fund, .tap-attune-clear {
  font-family: inherit;
  font-size: 9.5px;
  letter-spacing: 1px;
  padding: 6px 10px;
  border-radius: 2px;
  cursor: pointer;
  background: rgba(255, 255, 255, .04);
  color: var(--green);
  border: 1px solid var(--green-dim);
}
.tap-attune-fund:hover, .tap-attune-clear:hover { border-color: var(--amber); color: var(--amber); }
.tap-attune-shards { font-size: 9px; opacity: .6; }

/* ── Controller-nav focus ring (D2) — every element getNavCells() registers
   (the sub-tab buttons + now every visual card container, plus the RUNS select
   and FUND/CLEAR buttons) needs a VISIBLE ring when nav.js adds .ui-focused, or
   d-pad focus moves invisibly. Mirrors the .cell.ui-focused recipe in
   ui/loadout/loadout.css:233-257 (outline + inset glow) as our own selector list
   — tapestry.css intentionally never references loadout.css classes directly. */
.tap-tab.ui-focused,
.tap-attune-runs.ui-focused,
.tap-attune-fund.ui-focused,
.tap-attune-clear.ui-focused,
.tap-hide-woven.ui-focused,
.tap-source-launch.ui-focused,
.tap-star.ui-focused,
.tap-diagram-step.ui-focused,
.tap-elem-card.ui-focused,
.tap-react-card.ui-focused,
.tap-det-card.ui-focused,
.tap-card.ui-focused,
.tap-variant-card.ui-focused,
.tap-ability-card.ui-focused,
.tap-ladder-card.ui-focused,
.tap-named-gun-btn.ui-focused,
.tap-named-card.ui-focused {
  outline: 2.5px solid var(--amber);
  outline-offset: -2.5px;
  box-shadow: 0 0 10px var(--amber-dim, rgba(255, 176, 0, .3)) inset;
}

/* ── THE WEAVE — earned lore cards in books (tapestry-lore-spec.md §3/§6) ──
   Cards are PATCHES stitched onto the woven-cloth backdrop (the backdrop tile is
   painted once in tapestry.js and applied as a repeating background on .tap-body).
   Locked cards literally dangle a loose thread (::after). */
.tap-tab { position: relative; }
.tap-tab-pip {
  margin-left: 5px;
  font-size: 8px;
  letter-spacing: .5px;
  color: #1a1205;
  background: var(--rarity-exotic, #ffd34d);
  border-radius: 2px;
  padding: 1px 4px;
  vertical-align: 2px;
}
.tap-book { margin-top: 6px; }
.tap-book-spine {
  border: 1px solid var(--book-accent, var(--amber));
  border-left: 4px solid var(--book-accent, var(--amber));
  border-radius: 2px;
  padding: 10px 12px;
  background: rgba(255, 255, 255, .02);
  margin-bottom: 8px;
}
.tap-book-complete .tap-book-spine {
  border-color: var(--rarity-exotic, #ffd34d);
  box-shadow: 0 0 10px rgba(255, 211, 77, .25);
}
.tap-lore-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 12px 10px; }
.tap-lore-card {
  display: flex;
  align-items: center;
  gap: 10px;
  border: 1px solid var(--lore-accent, var(--green-dim));
  border-radius: 3px;
  padding: 9px 11px;
  background: rgba(5, 9, 22, .78);      /* patch: opaque enough to sit ON the cloth */
  cursor: pointer;
  position: relative;
  /* stitched border — the patch is sewn on */
  outline: 1px dashed color-mix(in srgb, var(--lore-accent, #33ff66) 45%, transparent);
  outline-offset: -4px;
}
.tap-lore-card:hover { border-color: var(--amber); }
.tap-lore-portrait {
  width: clamp(48px, 6vw, 72px);
  aspect-ratio: 1;
  flex-shrink: 0;
  display: grid;
  place-items: center;
  border: 1.5px solid color-mix(in srgb, var(--lore-accent, #33ff66) 60%, transparent);
  border-radius: 8px;
  background: radial-gradient(120% 100% at 30% 20%, color-mix(in srgb, var(--lore-accent, #33ff66) 22%, transparent), rgba(5, 9, 22, .92) 78%);
}
.tap-lore-portrait > svg { width: 78%; height: 78%; color: var(--lore-accent, var(--green)); }
.tap-lore-body { flex: 1 1 auto; min-width: 0; }
.tap-lore-head { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
.tap-lore-title { color: var(--lore-accent, var(--amber)); font-size: 11px; letter-spacing: 1.2px; }
.tap-lore-new {
  font-size: 7.5px; letter-spacing: 1px; padding: 1px 4px; border-radius: 2px;
  color: #1a1205; background: var(--rarity-exotic, #ffd34d);
}
.tap-lore-sub { font-size: 9px; opacity: .7; margin-top: 3px; line-height: 1.35; text-transform: none; }

/* Locked: a loose, dangling patch — dashed, desaturated, thread hanging off the
   bottom edge (a drooping curve + free end). Static; nothing animates at rest. */
.tap-lore-locked { border-style: dashed; opacity: .8; }
.tap-lore-locked .tap-lore-portrait > svg { filter: grayscale(.85) brightness(.7); opacity: .55; }
.tap-lore-locked .tap-lore-title { opacity: .75; }
.tap-lore-locked::after {
  content: '';
  position: absolute;
  left: 18px;
  bottom: -11px;
  width: 26px;
  height: 12px;
  border: 1.5px solid color-mix(in srgb, var(--lore-accent, #33ff66) 55%, transparent);
  border-top: none;
  border-right: none;
  border-radius: 0 0 0 90%;
  pointer-events: none;
}
.tap-lore-locked::before {
  content: '';
  position: absolute;
  left: 42px;
  bottom: -13px;
  width: 4px;
  height: 4px;
  border-radius: 50%;
  background: color-mix(in srgb, var(--lore-accent, #33ff66) 60%, transparent);
  pointer-events: none;
}
.tap-book-spine.ui-focused,
.tap-lore-card.ui-focused {
  outline: 2.5px solid var(--amber);
  outline-offset: -2.5px;
  box-shadow: 0 0 10px var(--amber-dim, rgba(255, 176, 0, .3)) inset;
}
:root.eff-w-le-900 .tap-lore-grid { grid-template-columns: 1fr; }

/* ── THE WEAVE — comic-splash lore reader (lore-reader.js, spec §6c) ────────
   Full-screen page over everything (loadout overlay is z30, its modals z50).
   Background = one-shot canvas paint (cosmos/rays/halftone); content column on
   top; caption boxes read as 1970s comic captions. */
.lore-reader {
  position: fixed;
  inset: 0;
  z-index: 60;
  display: flex;
  flex-direction: column;
  background: #05071a;
  color: var(--green);
}
.lr-splash { position: absolute; inset: 0; width: 100%; height: 100%; }
.lr-page {
  --lr-accent: var(--amber);
  position: relative;
  flex: 1;
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 10px;
  padding: 26px 18px 18px;
  max-width: 720px;
  width: 100%;
  margin: 0 auto;
}
.lr-tagrow { display: flex; gap: 10px; align-items: center; }
.lr-book-tag { font-size: 9px; letter-spacing: 2.5px; opacity: .65; }
.lr-ring-tag { font-size: 8px; letter-spacing: 2px; padding: 2px 7px; border-radius: 2px; border: 1px solid; }
.lr-ring-surface { color: var(--green); border-color: var(--green-dim); }
.lr-ring-mid { color: var(--sort-accent, #4fd6ff); border-color: var(--sort-accent, #4fd6ff); }
.lr-ring-deep { color: var(--rarity-exotic, #ffd34d); border-color: var(--rarity-exotic, #ffd34d); }
.lr-portrait {
  width: clamp(120px, 24vh, 200px);
  aspect-ratio: 1;
  display: grid;
  place-items: center;
  filter: drop-shadow(0 0 18px color-mix(in srgb, var(--lr-accent) 45%, transparent));
}
.lr-portrait > svg { width: 100%; height: 100%; color: var(--lr-accent); }
.lr-portrait-locked > svg { filter: grayscale(.85) brightness(.6); opacity: .5; }
/* Kirby title slab — big condensed caps, hard offset-print drop. */
.lr-title {
  font-size: clamp(22px, 5vw, 40px);
  letter-spacing: 3px;
  font-weight: 700;
  text-align: center;
  color: var(--lr-accent);
  text-shadow: 3px 3px 0 rgba(0, 0, 0, .9), -1px -1px 0 rgba(255, 255, 255, .12);
  text-transform: uppercase;
  line-height: 1.05;
}
.lr-kicker { font-size: 11px; letter-spacing: 1.5px; opacity: .75; font-style: italic; text-transform: none; }
.lr-captions { display: flex; flex-direction: column; gap: 10px; width: 100%; max-width: 560px; margin-top: 8px; }
.lr-caption {
  /* Dark ink panel (playtest 2026-07-18 — parchment-white boxes read as glare
     against the cosmos): deep indigo slab, accent spine, light text. */
  background: linear-gradient(160deg, rgba(16, 22, 52, .94), rgba(7, 10, 30, .96));
  color: #dfe6ff;
  border: 1.5px solid color-mix(in srgb, var(--lr-accent) 45%, transparent);
  border-left: 4px solid var(--lr-accent);
  box-shadow: 3px 3px 0 rgba(0, 0, 0, .6), inset 0 0 22px rgba(0, 0, 0, .35);
  padding: 12px 15px;
  /* R3 call 28: 12px read too small for the room an edge-to-edge page leaves
     (tapestry.css history — was 12px flat). */
  font-size: 15px;
  line-height: 1.5;
  letter-spacing: .4px;
  text-transform: none;
  text-shadow: 0 1px 0 rgba(0, 0, 0, .8);
}
.lr-caption::first-letter { color: var(--lr-accent); }
/* Alternate captions tilt slightly — pasted-on comic energy (transform only, static). */
.lr-caption:nth-child(odd)  { transform: rotate(-.4deg); }
.lr-caption:nth-child(even) { transform: rotate(.35deg); align-self: flex-end; max-width: 92%; }
.lr-locked { display: flex; flex-direction: column; align-items: center; gap: 8px; margin-top: 12px; text-align: center; }
.lr-locked-thread { font-size: 34px; color: var(--lr-accent); opacity: .7; }
.lr-locked-label { font-size: 10px; letter-spacing: 2.5px; opacity: .7; }
.lr-locked-deed {
  border: 1px dashed var(--lr-accent);
  border-radius: 2px;
  padding: 8px 14px;
  font-size: 11px;
  letter-spacing: .5px;
  color: var(--lr-accent);
  max-width: 480px;
  text-transform: none;
}
.lr-footer {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 14px;
  padding: 10px 14px calc(10px + env(safe-area-inset-bottom, 0px));
  border-top: 1px solid rgba(51, 255, 102, .18);
  background: rgba(5, 7, 26, .82);
}
.lr-nav-btn {
  font-family: inherit;
  font-size: 10px;
  letter-spacing: 1.5px;
  padding: 8px 14px;
  background: rgba(255, 255, 255, .04);
  color: var(--green);
  border: 1px solid var(--green-dim);
  border-radius: 2px;
  cursor: pointer;
  min-height: 40px;
}
.lr-nav-btn:hover, .lr-nav-btn.ui-focused { border-color: var(--amber); color: var(--amber); }
.lr-counter { font-size: 9px; letter-spacing: 2px; opacity: .7; }
.lr-close { margin-left: 10px; }

/* ── Nav arrows (comic-cutscenes-spec §11.1) ───────────────────────────────
   The click affordance the footer alone never taught: edge chevrons pinned at
   mid-height, over the splash but under nothing. A ←/→ press from ANY source
   lights the matching one (.lr-nav-hit, duration from config.comics.NAV_FLASH_MS
   via --lr-nav-ms), so a d-pad press shows the mouse where to go. */
.lr-arrow {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  z-index: 2;
  width: 46px;
  height: 84px;
  display: grid;
  place-items: center;
  font-family: inherit;
  font-size: 22px;
  line-height: 1;
  color: var(--green);
  background: rgba(5, 7, 26, .5);
  border: 1px solid var(--green-dim);
  border-radius: 3px;
  cursor: pointer;
  opacity: .5;
  transition: opacity 140ms ease, color 140ms ease, border-color 140ms ease;
}
.lr-arrow-prev { left: max(6px, env(safe-area-inset-left, 0px)); }
.lr-arrow-next { right: max(6px, env(safe-area-inset-right, 0px)); }
.lr-arrow:hover, .lr-arrow.ui-focused { opacity: 1; color: var(--amber); border-color: var(--amber); }
/* Nothing behind the first beat — the back affordance says so rather than lying. */
.lr-nav-dim { opacity: .16; pointer-events: none; }
.lr-arrow.lr-nav-dim { opacity: .12; }
/* The input echo. Informational, so it is NOT suppressed by reduced-motion —
   only its duration is tunable. */
@keyframes lr-nav-hit {
  0%   { color: #fff6e2; border-color: var(--amber); box-shadow: 0 0 0 0 var(--amber); opacity: 1; }
  100% { box-shadow: 0 0 14px 3px rgba(255, 176, 0, 0); }
}
.lr-nav-hit { animation: lr-nav-hit var(--lr-nav-ms, 260ms) ease-out 1; }
/* The page column is max-width 720; below ~900 the chevrons would sit ON the art
   instead of beside it. There they hide and the footer ◀ PREV / NEXT ▶ carry the
   job — those flash identically, so the input echo survives on phones. */
:root.eff-w-le-900 .lr-arrow { display: none; }

/* ── Enter / exit crossfade (comic-cutscenes-spec §11.2) ───────────────────
   Playtest 2026-08-04: "very jarring to have a cutscene instantly play." The
   sim is frozen behind the reader, so this dissolves over a still frame; the
   exit fade holds the pause until it finishes (lore-reader.js _finishClose). */
@keyframes lr-fade-in  { from { opacity: 0; transform: scale(1.015); } to { opacity: 1; transform: none; } }
@keyframes lr-fade-out { from { opacity: 1; transform: none; } to { opacity: 0; transform: scale(1.02); } }
.lore-reader.lr-fade-in  { animation: lr-fade-in  var(--lr-fade-ms, 420ms) ease-out 1; }
.lore-reader.lr-fade-out { animation: lr-fade-out var(--lr-fade-ms, 300ms) ease-in 1 forwards; }
@media (prefers-reduced-motion: reduce) {
  .lore-reader.lr-fade-in, .lore-reader.lr-fade-out { animation: none; }
}

/* THE KILL SPLASH (comic-cutscenes-spec §11.6) — the boss outro whose first page is
   the boss coming apart does NOT dissolve in. It SLAMS: overscaled by a hair and
   snapped back, hard easing, opacity up almost immediately. The §11.2 dissolve
   exists because a cutscene ambushing PLAY is jarring; this one is the payoff for a
   kill you just earned, and it should hit like the last shot did. Duration comes
   from config.comics.KILL_SPLASH_IN_MS via --lr-fade-ms (set by lore-reader.js).
   Reduced-motion falls through to the rule above: no animation at all, a clean cut. */
@keyframes lr-slam-in {
  0%   { opacity: 0; transform: scale(1.14); }
  55%  { opacity: 1; transform: scale(1.02); }
  100% { opacity: 1; transform: none; }
}
.lore-reader.lr-slam.lr-fade-in { animation: lr-slam-in var(--lr-fade-ms, 200ms) cubic-bezier(.2, .9, .25, 1) 1; }

/* Page-turn: incoming page slides from the turn direction (duration from
   config.tapestry.LORE_PAGE_TURN_MS via --lr-turn-ms). */
@keyframes lr-turn-right { from { opacity: 0; transform: translateX(26px); } to { opacity: 1; transform: none; } }
@keyframes lr-turn-left  { from { opacity: 0; transform: translateX(-26px); } to { opacity: 1; transform: none; } }
.lr-turn-right { animation: lr-turn-right var(--lr-turn-ms, 240ms) ease-out 1; }
.lr-turn-left  { animation: lr-turn-left  var(--lr-turn-ms, 240ms) ease-out 1; }
/* First-ever view: replay the gold weave-in on the whole page. */
@keyframes lr-fresh { 0% { filter: drop-shadow(0 0 0 rgba(255, 211, 77, 0)); } 18% { filter: drop-shadow(0 0 22px rgba(255, 211, 77, .8)); } 100% { filter: drop-shadow(0 0 0 rgba(255, 211, 77, 0)); } }
.lr-fresh { animation: lr-fresh 1600ms ease-out 1; }
@media (prefers-reduced-motion: reduce) {
  .lr-turn-right, .lr-turn-left, .lr-fresh { animation: none; }
}

/* ── THE FOES — the enemy compendium (docs/faction-card-spec.md §4) ─────────
   Grid cards are DOSSIER PLATES rather than woven patches: hard-edged, portrait-
   led, accent-spined by the order that fielded them. An un-fought entry is not a
   dangling thread (that's THE WEAVE's grammar for a locked CARD) — its gameplay
   half is fully present and only its portrait is desaturated, because the whole
   point of this tab is that the fight information was never gated. */
/* The poster cross-link — a slim bar under the hero. Deliberately quieter than
   an order header (dashed, no fill): it's an exit, not content, and it must not
   compete with the 21 dossiers it sits above. */
.tap-foe-poster {
  display: flex;
  align-items: center;
  gap: 10px;
  border: 1px dashed color-mix(in srgb, var(--rarity-exotic, #ffd34d) 55%, transparent);
  border-radius: 3px;
  padding: 9px 12px;
  margin-bottom: 4px;
  background: rgba(255, 211, 77, .04);
  text-decoration: none;
  color: inherit;
}
.tap-foe-poster:hover,
.tap-foe-poster.ui-focused {
  border-style: solid;
  border-color: var(--rarity-exotic, #ffd34d);
  box-shadow: 0 0 10px rgba(255, 211, 77, .22) inset;
}
.tap-foe-poster-body { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; gap: 3px; }
.tap-foe-poster-name { font-size: 10.5px; letter-spacing: 1.8px; color: var(--rarity-exotic, #ffd34d); }
.tap-foe-poster-sub { font-size: 9px; opacity: .7; line-height: 1.35; text-transform: none; }
.tap-foe-poster-arrow { flex: 0 0 auto; font-size: 15px; color: var(--rarity-exotic, #ffd34d); opacity: .8; }

.tap-foe-order { margin-top: 6px; }
.tap-foe-spine {
  border: 1px solid var(--order-accent, var(--amber));
  border-left: 4px solid var(--order-accent, var(--amber));
  border-radius: 2px;
  padding: 10px 12px;
  background: rgba(255, 255, 255, .02);
  margin-bottom: 8px;
}
.tap-foe-hand {
  font-size: 8.5px;
  letter-spacing: 2px;
  opacity: .7;
  color: var(--order-accent, var(--amber));
}
/* minmax tracks the portrait plate above: at 230px the enlarged silhouette ate the
   text column and wrapped "ATLAS TITAN" / clipped "SPARKBARREL". */
.tap-foe-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 10px; }
.tap-foe-card {
  display: flex;
  align-items: stretch;
  gap: 10px;
  border: 1px solid color-mix(in srgb, var(--foe-accent, #33ff66) 55%, transparent);
  border-left: 3px solid var(--role-color, var(--green));
  border-radius: 3px;
  padding: 8px 10px 8px 8px;
  background: rgba(5, 9, 22, .78);
  cursor: pointer;
  position: relative;
}
.tap-foe-card:hover { border-color: var(--amber); }
/* Portrait plate — a halftone-ish accent wash behind the rendered silhouette so
   the transparent PNG reads against the woven-cloth backdrop. */
.tap-foe-portrait {
  width: clamp(104px, 12vw, 140px);
  flex-shrink: 0;
  align-self: center;
  aspect-ratio: 4 / 5;
  display: grid;
  place-items: center;
  overflow: hidden;
  border: 1.5px solid color-mix(in srgb, var(--foe-accent, #33ff66) 60%, transparent);
  border-radius: 6px;
  background:
    radial-gradient(120% 100% at 30% 15%, color-mix(in srgb, var(--foe-accent, #33ff66) 26%, transparent), rgba(4, 7, 20, .95) 76%);
}
.foe-portrait-img { width: 100%; height: 100%; object-fit: contain; display: block; }
/* No WebGL → the placeholder keeps the plate's footprint so the grid never reflows. */
.foe-portrait-missing { display: block; width: 100%; height: 100%; }
/* Un-fought: a whisper of a dim, and NO grayscale. faction-card-spec.md §1.4 earns
   the lore blurb + epithet on first kill and nothing else — the silhouette is
   gameplay information (what am I about to fight?) and is never gated. The old
   grayscale(.8) desaturated the one axis that tells the three orders apart on a
   grid — Rust Kin ember, Atlas brass, Architects cyan-chrome — so a fresh profile
   rendered all 21 machines as the same grey blob. The locked lore line under the
   name is what says "you have not met this one." */
.tap-foe-unknown .tap-foe-portrait > .foe-portrait-img { filter: brightness(.9); opacity: .92; }

.tap-foe-body { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; gap: 4px; justify-content: center; }
.tap-foe-head { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
.tap-foe-name { color: var(--foe-accent, var(--amber)); font-size: 11.5px; letter-spacing: 1.4px; }
.tap-foe-chiprow { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
.tap-foe-role {
  font-size: 7.5px;
  letter-spacing: 1.4px;
  padding: 2px 6px;
  border-radius: 2px;
  color: var(--role-color, var(--green));
  border: 1px solid color-mix(in srgb, var(--role-color, #33ff66) 65%, transparent);
}
.tap-foe-tally { font-size: 8px; letter-spacing: 1.2px; opacity: .55; }
.tap-foe-sub { font-size: 9px; opacity: .72; line-height: 1.35; text-transform: none; }
.tap-foe-unknown .tap-foe-sub { font-style: italic; opacity: .6; }
.tap-foe-spine.ui-focused,
.tap-foe-card.ui-focused {
  outline: 2.5px solid var(--amber);
  outline-offset: -2.5px;
  box-shadow: 0 0 10px var(--amber-dim, rgba(255, 176, 0, .3)) inset;
}
:root.eff-w-le-900 .tap-foe-grid { grid-template-columns: 1fr; }

/* ── THE FOES — the dossier sheet (foe-sheet.js) ────────────────────────────
   Rides .lore-reader's full-screen chrome (splash canvas / page column / footer);
   everything below is the parts a dossier has that a lore card doesn't. */
/* A dossier is taller than a lore card, so this column actually scrolls — and a
   default light scrollbar down the middle of a cosmos splash reads as a seam.
   Same treatment the vault drill + tab bar already use. */
.foe-sheet .fs-page { max-width: 760px; scrollbar-width: none; }
.foe-sheet .fs-page::-webkit-scrollbar { display: none; }

/* playtest-r3.md #26 — "THE FOES header gets the WOW treatment": the faction line
 * (ATLAS · RUST KIN…) sat at the shared 9px .lr-book-tag size under a title that
 * itself reads small for how much room the dossier gives it. Scoped overrides
 * ONLY inside .foe-sheet — .lr-book-tag/.lr-title are shared with the STORIES
 * comic reader (lore-reader.js) and must not move size there. */
.foe-sheet .lr-book-tag {
  font-size: 15px;
  letter-spacing: 3px;
  opacity: .9;
  font-weight: 700;
  color: var(--lr-accent);
}
.foe-sheet .lr-title {
  font-size: clamp(32px, 8vw, 56px);
  letter-spacing: 4px;
}
.fs-role-tag {
  font-size: 8px; letter-spacing: 2px; padding: 2px 7px; border-radius: 2px;
  color: var(--role-color, var(--green));
  border: 1px solid var(--role-color, var(--green));
}
.fs-portrait {
  width: clamp(150px, 30vh, 250px);
  aspect-ratio: 4 / 5;
  display: grid;
  place-items: center;
  filter: drop-shadow(0 0 20px color-mix(in srgb, var(--lr-accent) 45%, transparent));
}
.fs-portrait > .foe-portrait-img { width: 100%; height: 100%; object-fit: contain; }
.fs-body { width: 100%; max-width: 600px; display: flex; flex-direction: column; gap: 10px; margin-top: 6px; }

/* Stat block — label / live value / bar normalized against the roster max. */
.fs-stats { display: grid; gap: 5px; }
.fs-stat { display: grid; grid-template-columns: 74px 68px 1fr; align-items: center; gap: 8px; }
.fs-stat-k { font-size: 8.5px; letter-spacing: 1.6px; opacity: .7; }
.fs-stat-v { font-size: 11px; letter-spacing: .6px; color: var(--lr-accent); text-align: right; }
.fs-stat-bar { height: 6px; background: rgba(255, 255, 255, .07); border-radius: 2px; overflow: hidden; }
.fs-stat-fill { height: 100%; background: var(--lr-accent); opacity: .8; }

/* Attacks — each one a slab: what it does, then the TELL that beats it. */
.fs-attacks { display: flex; flex-direction: column; gap: 8px; }
.fs-attack {
  background: linear-gradient(160deg, rgba(16, 22, 52, .92), rgba(7, 10, 30, .95));
  border: 1px solid color-mix(in srgb, var(--lr-accent) 35%, transparent);
  border-left: 3px solid var(--lr-accent);
  padding: 8px 11px;
}
.fs-attack-name { font-size: 10px; letter-spacing: 1.6px; color: var(--lr-accent); }
.fs-attack-desc { font-size: 11px; line-height: 1.5; color: #dfe6ff; text-transform: none; margin-top: 3px; }
.fs-attack-tell { font-size: 10px; line-height: 1.45; opacity: .78; text-transform: none; margin-top: 5px; font-style: italic; }

.fs-line { font-size: 11px; line-height: 1.5; text-transform: none; }
.fs-line-label {
  display: inline-block; margin-right: 7px;
  font-size: 8.5px; letter-spacing: 1.8px; text-transform: uppercase; font-style: normal;
  color: var(--lr-accent); opacity: .85;
}
.fs-line-text { color: #dfe6ff; }
.fs-weak { border-left: 2px solid var(--lr-accent); padding-left: 9px; }
.fs-counter { border-left: 2px solid var(--rarity-exotic, #ffd34d); padding-left: 9px; }
.fs-counter .fs-line-label { color: var(--rarity-exotic, #ffd34d); }

/* The one earned element: the lore caption (reader grammar) or its deed line. */
.fs-lore { align-self: stretch; }
.fs-lore-locked {
  border-style: dashed;
  border-left-style: solid;
  color: var(--lr-accent);
  opacity: .8;
  font-style: italic;
  text-align: center;
}
.fs-tally { font-size: 8.5px; letter-spacing: 2px; opacity: .55; text-align: right; }

:root.eff-w-le-900 .fs-stat { grid-template-columns: 66px 60px 1fr; }

/* ── THE CHASE — the gear dossier sheet (gear-sheet.js) ─────────────────────
   Same chrome as the foe sheet above (.lore-reader splash / page / footer); the
   difference is what the body holds — a stat block, the two perk COLUMNS a roll
   draws from, and the flavor line. */
.gear-sheet .gs-page { max-width: 760px; scrollbar-width: none; }
.gear-sheet .gs-page::-webkit-scrollbar { display: none; }

/* The reserved model frame (gear-inspector-spec.md §3/§4). Today the item's own
   icon at display size; the live turntable drops into this same box later, which
   is why it's a sized frame with a place-items grid rather than a bare img. */
.gs-stage {
  width: clamp(170px, 30vh, 280px);
  aspect-ratio: 4 / 3;
  display: grid;
  place-items: center;
  padding: 10px;
  border: 1px solid color-mix(in srgb, var(--lr-accent) 30%, transparent);
  background: radial-gradient(ellipse at 50% 60%, color-mix(in srgb, var(--lr-accent) 14%, transparent), rgba(6, 9, 26, .55));
  filter: drop-shadow(0 0 20px color-mix(in srgb, var(--lr-accent) 40%, transparent));
}
.gs-stage-icon { width: 100%; height: 100%; object-fit: contain; }

.gs-body { width: 100%; max-width: 600px; display: flex; flex-direction: column; gap: 12px; margin-top: 6px; }

/* Stat block — label / value / the legendary roll band it lands inside. */
.gs-stats { display: grid; gap: 5px; }
/* The value column is sized for the WIDEST row a chest piece produces
   ("×1.15–×1.40"), and never wraps — a range broken across two lines reads as two
   numbers rather than as one band. */
.gs-stat { display: grid; grid-template-columns: 62px 96px 1fr; align-items: baseline; gap: 8px; }
.gs-stat-k { font-size: 8.5px; letter-spacing: 1.6px; opacity: .7; }
.gs-stat-v { font-size: 12px; letter-spacing: .6px; color: var(--lr-accent); text-align: right; white-space: nowrap; }
.gs-stat-n { font-size: 9px; letter-spacing: .8px; opacity: .55; text-transform: none; }

/* Perk pools — the actual chase. Two columns, each a slab of what CAN roll. */
.gs-perks { display: flex; flex-direction: column; gap: 10px; }
.gs-perk-note { font-size: 8.5px; letter-spacing: 1.6px; opacity: .6; line-height: 1.5; }
.gs-perk-col { display: flex; flex-direction: column; gap: 6px; }
.gs-perk-head { font-size: 9px; letter-spacing: 2px; color: var(--lr-accent); opacity: .9; }
.gs-perk-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); gap: 6px; }
.gs-perk-chip {
  background: linear-gradient(160deg, rgba(16, 22, 52, .92), rgba(7, 10, 30, .95));
  border: 1px solid color-mix(in srgb, var(--lr-accent) 30%, transparent);
  border-left: 3px solid var(--lr-accent);
  padding: 7px 9px;
}
.gs-perk-name { font-size: 9.5px; letter-spacing: 1.4px; color: var(--lr-accent); }
.gs-perk-desc { font-size: 10.5px; line-height: 1.45; color: #dfe6ff; text-transform: none; margin-top: 3px; }

/* Flavor — somebody's voice, never the narrator, never mechanical (§7.2). */
.gs-flavor { align-self: stretch; }
.gs-flavor-line { font-style: italic; text-transform: none; }
.gs-flavor-src { font-size: 9px; letter-spacing: 1.2px; opacity: .6; text-align: right; margin-top: 6px; text-transform: none; }

:root.eff-w-le-900 .gs-stat { grid-template-columns: 56px 88px 1fr; }
:root.eff-w-le-900 .gs-perk-grid { grid-template-columns: 1fr; }

/* ── Responsive: phones ──────────────────────────────────────────────────── */
@media (max-width: 640px) {
  .tap-body { padding: 10px 10px 20px; }
  .tap-tab { font-size: 10px; padding: 7px 10px; }
}

/* Mobile-first cards (docs/ui-scale-vault-view.md eff-w contract). On phones /
   small tablets (effW ≤ 900) every Tapestry card grid collapses to one
   full-width column — a two-up card on a phone is unreadable. Wide desktops
   (effW > 900) keep the auto-fit multi-column layout and lean into the larger,
   icon-forward cards. Uses the class-gated breakpoint, not @media, because UI
   SCALE zooms the overlay so raw-viewport queries are off by the scale factor. */
:root.eff-w-le-900 .tap-elem-grid,
:root.eff-w-le-900 .tap-react-grid,
:root.eff-w-le-900 .tap-det-grid,
:root.eff-w-le-900 .tap-chassis-grid,
:root.eff-w-le-900 .tap-variant-grid,
:root.eff-w-le-900 .tap-ability-grid,
:root.eff-w-le-900 .tap-perk-grid,
:root.eff-w-le-900 .tap-ladder-grid,
:root.eff-w-le-900 .tap-named-grid,
:root.eff-w-le-900 .tap-exotic-grid {
  grid-template-columns: 1fr;
}

/* EXOTIC is the aspirational identity item — always span the full ladder width,
   even on desktop, so it reads as more important than the other rarities. */
.tap-ladder-card.tap-ladder-exotic { grid-column: 1 / -1; }

/* ── STORIES — the comics library tab + the reader's issue mode ─────────────
   (comic-cutscenes-spec.md §10). Issue cards mirror the WEAVE's lore-card
   grammar: unlocked = live cover; locked = silhouetted cover + ⌇ tease. */
.tap-stories { display: flex; flex-direction: column; gap: 14px; }
.tap-stories-fallback {
  font-size: 11px;
  letter-spacing: 1.5px;
  opacity: .75;
  text-align: center;
  padding: 26px 12px 8px;
}
.tap-stories-retry {
  font-family: inherit;
  font-size: 10px;
  letter-spacing: 1.5px;
  padding: 8px 18px;
  align-self: center;
  background: rgba(255, 255, 255, .04);
  color: var(--green);
  border: 1px solid var(--green-dim);
  border-radius: 2px;
  cursor: pointer;
  min-height: 40px;
}
.tap-stories-retry:hover, .tap-stories-retry.ui-focused { border-color: var(--amber); color: var(--amber); }
.tap-story-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
  gap: 14px;
}
/* Spine header over an arc's issues (campaign-spec §8 item 6). */
.tap-story-arc-head {
  margin: 14px 0 8px;
  font-size: 11px; letter-spacing: 3px; color: var(--amber); opacity: 0.85;
  border-bottom: 1px solid rgba(255, 176, 0, 0.25);
  padding-bottom: 4px;
}
.tap-story-card {
  display: flex;
  flex-direction: column;
  gap: 6px;
  padding: 10px;
  background: rgba(8, 12, 34, .82);
  border: 1px solid rgba(255, 158, 77, .28);
  border-radius: 3px;
  cursor: pointer;
}
.tap-story-card.ui-focused, .tap-story-unlocked:hover { border-color: var(--amber); }
.tap-story-locked { cursor: default; }
.tap-story-cover {
  position: relative;
  aspect-ratio: 3 / 4;
  overflow: hidden;
  border-radius: 2px;
  background: #05071a;
}
.tap-story-cover img { width: 100%; height: 100%; object-fit: cover; display: block; }
/* Locked = cover-silhouette: the shape teases, the art stays sealed. */
.tap-story-locked .tap-story-cover img {
  filter: brightness(.14) saturate(.15) contrast(1.25);
}
.tap-story-locked .tap-story-cover::after {
  content: '⌇';
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  font-size: 34px;
  color: rgba(255, 158, 77, .8);
  text-shadow: 0 0 14px rgba(0, 0, 0, .9);
}
.tap-story-new {
  position: absolute;
  top: 6px;
  right: 6px;
  font-size: 9px;
  letter-spacing: 1.5px;
  padding: 2px 7px;
  background: rgba(5, 7, 26, .85);
  color: var(--amber);
  border: 1px solid var(--amber);
  border-radius: 2px;
}
.tap-story-head { display: flex; align-items: baseline; gap: 8px; }
.tap-story-issue { font-size: 9px; letter-spacing: 2px; opacity: .65; }
.tap-story-title { font-size: 12px; letter-spacing: 1.5px; font-weight: 700; }
.tap-story-locked .tap-story-title { opacity: .8; }
.tap-story-sub { font-size: 10px; letter-spacing: .4px; opacity: .75; text-transform: none; line-height: 1.45; }
.tap-story-locked .tap-story-sub { color: #ff9e4d; opacity: .85; }
:root.eff-w-le-900 .tap-story-grid { grid-template-columns: repeat(2, 1fr); }

/* playtest-r3.md #27 — a mission's intro/outro pair, boxed under one label instead
 * of reading as two loose siblings in the arc's flat grid. Spans every column of
 * the outer .tap-story-grid (a group is a row of its own); its own nested grid
 * holds just that mission's 1-2 cards, capped narrow so a pair doesn't stretch
 * into full-width cards on a wide viewport. */
.tap-story-mission-group {
  grid-column: 1 / -1;
  padding: 10px 12px 12px;
  border: 1px solid rgba(255, 176, 0, .18);
  border-radius: 4px;
  background: rgba(255, 176, 0, .03);
}
.tap-story-mission-label {
  font-size: 10px;
  letter-spacing: 2.2px;
  color: var(--amber);
  opacity: .8;
  margin-bottom: 8px;
}
.tap-story-mission-cards {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(160px, 220px));
  gap: 14px;
}

/* Reader issue mode (lore-reader.js openIssue): served art page above the
   caption plate — art is text-free, the plate carries the lettering.
   Responsive pass (2026-08-02): .lr-page-issue is HEIGHT-FIT — the art is the
   flexible child and shrinks-to-fit so the whole page image AND its caption
   plate share the viewport on any screen; nothing scrolls out of reach. The
   frame lives on the <img> (not the box) so it always hugs the drawn page. */
.lr-page-issue { overflow: hidden; }
.lr-comicart {
  flex: 1 1 auto;
  min-height: 0;              /* let flex actually shrink the art, not the captions */
  width: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
}
.lr-comicart img {
  display: block;
  width: auto;
  height: auto;
  max-width: min(100%, 560px);
  max-height: 100%;           /* the fit rule: never taller than the space the captions leave */
  border: 2px solid rgba(0, 0, 0, .85);
  box-shadow: 4px 4px 0 rgba(0, 0, 0, .6);
  background: #05071a;
}
.lr-page-issue .lr-captions { flex: 0 0 auto; }

/* THREAD VIEW (docs/smart-panel-research.md — pilot): the art box becomes a
   crop viewport and the full-page <img> pans/zooms inside it, transform-only
   (translate3d + scale set inline by lore-reader.js; durations from
   config.comics.THREAD_PAN_MS / THREAD_PULLBACK_MS). The comic frame moves
   from the img to the box — the img is now the camera, not the page. */
.lr-comicart.lr-threadview {
  position: relative;
  overflow: hidden;
  align-items: stretch;
  justify-content: flex-start;
  width: 100%;
  max-width: 560px;
  margin: 0 auto;
  border: 2px solid rgba(0, 0, 0, .85);
  box-shadow: 4px 4px 0 rgba(0, 0, 0, .6);
  background: #05071a;
}
.lr-comicart.lr-threadview img {
  position: absolute;
  top: 0;
  left: 0;
  max-width: none;
  max-height: none;
  border: 0;
  box-shadow: none;
  transform-origin: 0 0;
}
@media (prefers-reduced-motion: reduce) {
  .lr-comicart.lr-threadview img { transition: none !important; }
}

/* Issue end plate (playtest 2026-08-03): THE END slab centered in the art's
   space — the reader always lands here before CONTINUE hands back control. */
.lr-page-issue .lr-end {
  flex: 1 1 auto;
  min-height: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 10px;
}

/* ── EDGE-TO-EDGE (comic-cutscenes-spec §11.5, 2026-08-09) ─────────────────
   A campaign chapter opens with its page filling the screen. The default issue
   layout is a 720px column with the art capped at 560px — correct for browsing
   the STORIES shelf, far too small for the beat that opens a mission. Here the
   art becomes the PAGE: absolutely positioned across the whole reader, contain-
   fit (never cropped, so a lettering-free panel can't lose its composition),
   with every piece of chrome floating over it on a scrim.

   Presentation only — no JS layout branch. The thread-view panel solver already
   measures the art box at runtime, so it re-solves against the bigger viewport
   for free; the caption plate is still HTML, so the words are still text. */
.lore-reader.lr-cinematic .lr-page {
  max-width: none;
  padding: 0;
  gap: 0;
  overflow: hidden;
}
/* The art is the page. Behind everything; each overlay lifts itself above it. */
.lore-reader.lr-cinematic .lr-comicart {
  position: absolute;
  inset: 0;
  width: auto;
  max-width: none;
  margin: 0;
  z-index: 0;
}
.lore-reader.lr-cinematic .lr-comicart img {
  max-width: 100%;
  max-height: 100%;
  border: 0;
  box-shadow: none;
  background: transparent;
}
/* Thread view: the crop viewport is now the screen, and its comic frame would
   be a border around the whole display — drop it. */
.lore-reader.lr-cinematic .lr-comicart.lr-threadview {
  max-width: none;
  border: 0;
  box-shadow: none;
}

/* Top chrome (issue tag + Kirby title slab) rides a scrim over the art rather
   than stealing height from it. */
.lore-reader.lr-cinematic .lr-tagrow,
.lore-reader.lr-cinematic .lr-title {
  position: relative;
  z-index: 2;
  width: 100%;
  text-align: center;
  pointer-events: none;
}
.lore-reader.lr-cinematic .lr-tagrow {
  justify-content: center;
  padding: calc(14px + env(safe-area-inset-top, 0px)) 18px 8px;
  background: linear-gradient(180deg, rgba(5, 7, 26, .88), rgba(5, 7, 26, 0));
}
/* The tag row now sits ON the page art instead of on the reader's own backdrop,
   and a parchment panel is the one background these small dim glyphs vanish
   into. The shadow is what keeps them readable on a light page. */
.lore-reader.lr-cinematic .lr-tagrow > * {
  text-shadow: 0 1px 4px rgba(0, 0, 0, .95), 0 0 2px rgba(0, 0, 0, .9);
  opacity: 1;
}
.lore-reader.lr-cinematic .lr-title {
  padding: 6px 18px 22px;
  font-size: clamp(26px, 6vw, 62px);
  background: linear-gradient(180deg, rgba(5, 7, 26, .72), rgba(5, 7, 26, 0));
  margin-top: -8px;
}

/* Caption plate: pinned to the bottom of the art, clear of the footer chrome,
   widened for the bigger canvas. The alternating tilt stays — it's the comic
   grammar — but a lone plate no longer drifts to one side. */
.lore-reader.lr-cinematic .lr-captions {
  position: absolute;
  left: 50%;
  transform: translateX(-50%);
  bottom: 12px;
  z-index: 2;
  width: min(92%, 860px);
  max-width: none;
  margin: 0;
  align-items: center;
}
.lore-reader.lr-cinematic .lr-caption {
  width: 100%;
  max-width: none;
  align-self: stretch;
  /* R3 call 28: was clamp(12px, 1.6vw, 16px) — the 1.6vw preferred value never
     clears 12px below ~750px wide, so every phone rendered at the floor and
     the edge-to-edge page (the whole reason the plate has room to grow) never
     used it. Both ends raised; the phone floor is now the number that matters. */
  font-size: clamp(16px, 2.4vw, 21px);
  backdrop-filter: blur(2px);
}
/* THE END plate keeps its centered slab, just over the full-bleed backdrop. */
.lore-reader.lr-cinematic .lr-end { z-index: 2; }

/* Footer floats: no top border, no fill bar cutting a black line across the
   art. The default button fill is rgba(255,255,255,.04) — invisible over a lit
   comic panel — so in cinematic they carry their own dark plate instead of
   relying on the bar behind them. */
.lore-reader.lr-cinematic .lr-footer {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 3;
  border-top: 0;
  background: linear-gradient(0deg, rgba(5, 7, 26, .9), rgba(5, 7, 26, .55) 55%, rgba(5, 7, 26, 0));
}
.lore-reader.lr-cinematic .lr-nav-btn { background: rgba(5, 7, 26, .82); }
.lore-reader.lr-cinematic .lr-counter { opacity: .9; text-shadow: 0 1px 3px rgba(0, 0, 0, .9); }
/* …which means the captions must clear it. 60px is the footer's own height
   (10px padding × 2 + the nav buttons); the inset is added on top because the
   footer grows by exactly that much on a notched phone. */
.lore-reader.lr-cinematic .lr-captions { bottom: calc(68px + env(safe-area-inset-bottom, 0px)); }
.lore-reader.lr-cinematic .lr-arrow { z-index: 3; }

/* CAPTION BEATS (comic-cutscenes-spec §10.5): the caption plate steps through
   authored beats[] over a static full page. Each step fades the new line in
   (duration set inline from config.comics.BEAT_STEP_MS; the class is
   re-added per step by lore-reader.js to restart the animation). */
.lr-caption.lr-cap-step { animation: lr-cap-step var(--lr-cap-ms, 180ms) ease; }
@keyframes lr-cap-step {
  from { opacity: 0; translate: 0 4px; }
  to   { opacity: 1; translate: 0 0; }
}
@media (prefers-reduced-motion: reduce) {
  .lr-caption.lr-cap-step { animation: none !important; }
}
