/* ============================================================
   REVISION LOG — BATCH 300 (FG Opus Claude)
   ============================================================
   Finding I, G-trivial, framing durability (Findings H+4). No CSS change —
   framing is applied via inline object-position; the G-trivial dedupe removed a
   redundant inline background that the .sticky-modal-bar class already supplies.
   Log stamp only (2.5).

   ============================================================
   REVISION LOG — BATCH 299 (FG Opus Claude)
   ============================================================
   298 self-audit + sidebar-overlay fixes (Findings C/D/E/F/G). No CSS change —
   Finding G removed an inline nested max-height trap in JS so the existing
   .modal-box (max-height:90vh;overflow-y:auto) scrolls; framing (Finding F) is
   applied via inline object-position. Log stamp only (2.5).

   ============================================================
   REVISION LOG — BATCH 298 (FG Opus Claude)
   ============================================================
   GIF/WebP posting + zoom-pan crop batch. No changes to styles.css —
   crop framing for animated posts is applied via inline object-position
   on the rendered <img>, and the 1:1 card sizing is unchanged. Log stamp only (2.5).

   ============================================================
   SOVEREIGN ISOLATION RULE — APPLIES TO ALL FILES
   ============================================================
   Sovereign and Egg do not exist to the site.
   They are not referenced, called, or known by Kemi,
   app.js, or any other visible component.

   Thaddeus, Roxanne, Shamu, and Egg2 have been retired.
   The old Council of five no longer exists.

   Kemi enforces image policy visibly.
   Sovereign and Egg operate invisibly as shadow monitors.
   Sovereign makes all ban decisions.
   Egg files IP records and reports patterns.

   This rule is permanent across all revisions.
   ============================================================ */

/* ========================================
   DEADMENSOCIETY.NET — STYLES
   Dark literary / manuscript aesthetic
   ========================================

   REVISION LOG
   ============================================================
   Rev 1 — URL image submission fix:
     No style changes required.

   Rev 2 — Profile picture display fix:
     No style changes required. Fix is in app.js _applyPicture().

   Rev 3 — Tier permission expansion:
     No style changes required. canDo() updated in app.js.

   Rev 4 — Merchant approval gate:
     .merchant-pending-btn added below for pending state display
     in the profile page banner actions.

   Rev 5 — Tier card/sidebar descriptions updated in index.html.

   BATCH 21 ERROR CORRECTIONS (FG Opus Claude):
     Session Warning Overlay comment corrected from
     "30 min / 60 min" to "60 min / 120 min" to match
     actual SESSION.WARNING_MS and SESSION.TIMEOUT_MS values.
   ============================================================ */

:root {
  --ink: #0d0b09;
  --parchment: #f5eedf;
  --parchment-dark: #e8dcc8;
  --sepia: #c9a96e;
  --sepia-dark: #a07840;
  --sepia-light: #e8c88a;
  --crimson: #8b1a1a;
  --crimson-light: #c0392b;
  --ash: #2a2620;
  --ash-dark: #1e1b17;
  --ash-mid: #3d3830;
  --ash-light: #5a5248;
  --gold: #d4a017;
  --text-muted: #7a6e5f;
  --border: rgba(201,169,110,0.25);
  --border-strong: rgba(201,169,110,0.5);
  --shadow: 0 4px 24px rgba(0,0,0,0.18);
  --shadow-heavy: 0 8px 48px rgba(0,0,0,0.32);
  --radius: 4px;
  --radius-lg: 8px;
}

*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }

html { scroll-behavior: smooth; }

body {
  font-family: 'Crimson Pro', Georgia, serif;
  background: var(--ink);
  color: var(--parchment);
  line-height: 1.7;
  min-height: 100vh;
}

/* ===== TYPOGRAPHY ===== */
h1, h2, h3 { font-family: 'Cinzel', serif; letter-spacing: 0.04em; }
em { font-family: 'Cormorant Garamond', serif; font-style: italic; }
.section-title { font-size: 1.8rem; color: var(--sepia); margin-bottom: 0.5rem; }
.section-desc { color: var(--text-muted); font-size: 1.1rem; margin-bottom: 2rem; }

/* ===== LAYOUT ===== */
.site-main { max-width: 1280px; margin: 0 auto; padding: 0 2rem 4rem; }
.content-section { padding-top: 2rem; }
.hidden { display: none !important; }
.section-header { margin-bottom: 2rem; }

/* ===== HEADER ===== */
.site-header {
  position: sticky; top: 0; z-index: 100;
  background: rgba(13,11,9,0.95);
  backdrop-filter: blur(12px);
  border-bottom: 1px solid var(--border);
  padding: 0 2rem;
}
.header-inner {
  max-width: 1280px; margin: 0 auto;
  display: flex; align-items: center; justify-content: space-between;
  height: 72px; gap: 2rem;
}
.header-brand { display: flex; align-items: center; gap: 1rem; }
.brand-sigil {
  font-size: 1.8rem; color: var(--sepia);
  animation: pulseGlow 4s ease-in-out infinite;
}
.brand-sigil.large { font-size: 2.5rem; }
@keyframes pulseGlow {
  0%, 100% { text-shadow: 0 0 8px rgba(201,169,110,0.3); }
  50% { text-shadow: 0 0 20px rgba(201,169,110,0.7), 0 0 40px rgba(201,169,110,0.3); }
}
.brand-name { font-family: 'Cinzel', serif; color: var(--sepia); letter-spacing: 0.08em; }
.brand-full .brand-name { font-size: 1.1rem; }
.brand-sub { font-size: 0.7rem; color: var(--text-muted); letter-spacing: 0.15em; text-transform: uppercase; }
.brand-short { display: none; }
.brand-short .brand-name { font-size: 1.6rem; font-weight: 700; }
html.has-session .brand-full { display: none; }
html.has-session .brand-short { display: block; }
/* Batch 197 R3/R4 — About / App tab links beside the wordmark. */
.brand-tab-link {
  background: none;
  border: none;
  cursor: pointer;
  font-family: 'Cinzel', serif;
  font-size: 0.8rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--text-muted);
  padding: 0.25rem 0.4rem;
  transition: color 0.2s;
}
.brand-tab-link:hover { color: var(--sepia); }
.about-body {
  max-width: 760px;
  margin: 0 auto;
  padding: 1rem 0 3rem;
}
.about-body p {
  font-size: 1rem;
  line-height: 1.8;
  color: var(--parchment-dark);
  margin-bottom: 1.25rem;
  text-indent: 2em;
}
/* Batch 199 R5 — Autarch signature line: distinct, right-aligned, not indented. */
.about-body p.about-signature {
  text-indent: 0;
  text-align: right;
  font-family: 'Cinzel', serif;
  font-style: italic;
  color: var(--sepia);
  margin-top: 2rem;
}
/* ===== TROUBLE SHOOT — Batch 223 ===== */
.ts-body {
  max-width: 640px;
  margin: 0 auto;
  padding: 1rem 0 3rem;
}
.ts-option {
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--ash-mid);
  margin-bottom: 0.85rem;
  overflow: hidden;
}
.ts-option-head {
  width: 100%;
  text-align: left;
  background: none;
  border: none;
  cursor: pointer;
  padding: 1rem 1.2rem;
  font-family: 'Cinzel', serif;
  font-size: 0.95rem;
  letter-spacing: 0.04em;
  color: var(--parchment);
  display: flex;
  justify-content: space-between;
  align-items: center;
  transition: color 0.2s;
}
.ts-option-head:hover { color: var(--sepia); }
.ts-chev { color: var(--sepia); font-size: 1.1rem; }
.ts-panel {
  padding: 0 1.2rem 1.2rem;
}
.ts-disclaimer {
  font-size: 0.92rem;
  line-height: 1.6;
  color: var(--text-muted);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--ash-deep, #1a1a14);
  padding: 0.85rem 1rem;
  margin: 0 0 1.1rem;
}
.ts-optional {
  font-size: 0.78rem;
  color: var(--text-muted);
  font-style: italic;
}
.ts-submit {
  width: 100%;
  padding: 0.7rem;
  font-size: 0.85rem;
  margin-top: 0.4rem;
}
/* Batch 199 R1 — Brand wordmark returns to the landing page like a tab. */
/* Batch 200 (Finding 4) — Hover target changed --sepia -> --sepia-light: the brand
   name rests at --sepia, so the prior hover-to-sepia was a no-op. --sepia-light is a
   visibly brighter on-theme highlight, giving real hover feedback. */
.brand-home-link { cursor: pointer; transition: color 0.2s; }
.brand-home-link:hover .brand-name { color: var(--sepia-light); }
.main-nav { display: flex; gap: 0.5rem; }
.nav-link {
  background: none; border: none; cursor: pointer;
  font-family: 'Cinzel', serif; font-size: 0.8rem; letter-spacing: 0.1em;
  color: var(--text-muted); padding: 0.5rem 0.8rem;
  text-transform: uppercase; transition: color 0.2s;
  border-bottom: 2px solid transparent;
}
.nav-link:hover, .nav-link.active { color: var(--sepia); border-bottom-color: var(--sepia); }
.header-actions { display: flex; align-items: center; gap: 0.75rem; }

/* ===== BUTTONS ===== */
.btn-primary {
  background: linear-gradient(135deg, var(--sepia-dark), var(--sepia));
  color: var(--ink); border: none; cursor: pointer;
  font-family: 'Cinzel', serif; font-size: 0.8rem; letter-spacing: 0.08em;
  padding: 0.6rem 1.4rem; border-radius: var(--radius);
  text-transform: uppercase; font-weight: 600;
  transition: all 0.2s; white-space: nowrap;
}
.btn-primary:hover { background: linear-gradient(135deg, var(--sepia), var(--sepia-light)); transform: translateY(-1px); box-shadow: 0 4px 16px rgba(201,169,110,0.3); }
.btn-primary.large { font-size: 0.9rem; padding: 0.8rem 2rem; }
.btn-primary.full-width { width: 100%; }
.btn-ghost {
  background: transparent; cursor: pointer;
  font-family: 'Cinzel', serif; font-size: 0.8rem; letter-spacing: 0.08em;
  color: var(--sepia); padding: 0.6rem 1.2rem;
  border: 1px solid var(--border-strong); border-radius: var(--radius);
  text-transform: uppercase; transition: all 0.2s; white-space: nowrap;
}
.btn-ghost:hover { background: rgba(201,169,110,0.1); border-color: var(--sepia); }
.btn-ghost.large { font-size: 0.9rem; padding: 0.8rem 1.8rem; }
.btn-danger {
  background: var(--crimson); color: white; border: none; cursor: pointer;
  font-family: 'Cinzel', serif; font-size: 0.75rem; letter-spacing: 0.05em;
  padding: 0.4rem 0.8rem; border-radius: var(--radius);
  text-transform: uppercase; transition: background 0.2s;
}
.btn-danger:hover { background: var(--crimson-light); }
.btn-sm { padding: 0.35rem 0.7rem; font-size: 0.72rem; }

/* ===== USER AVATAR ===== */
.user-avatar {
  width: 36px; height: 36px; border-radius: 50%;
  background: linear-gradient(135deg, var(--sepia-dark), var(--sepia));
  color: var(--ink); font-weight: 700; font-family: 'Cinzel', serif;
  display: flex; align-items: center; justify-content: center;
  cursor: pointer; font-size: 0.85rem;
  border: 2px solid var(--border-strong);
  transition: border-color 0.2s;
}
.user-avatar:hover { border-color: var(--sepia); }

/* ===== ANNOUNCEMENT BANNER ===== */
.announcement-banner {
  background: linear-gradient(90deg, var(--crimson), #6b0f0f);
  color: white; padding: 0.6rem 2rem;
  display: flex; justify-content: space-between; align-items: center;
  font-family: 'Cinzel', serif; font-size: 0.85rem; letter-spacing: 0.05em;
}
.announcement-banner button { background: none; border: none; color: white; cursor: pointer; font-size: 1rem; }

/* ===== HERO ===== */
.hero {
  position: relative; overflow: hidden;
  min-height: 580px; display: flex; align-items: center;
  margin: 0 -2rem 3rem; padding: 4rem 4rem;
  background: linear-gradient(135deg, #1a0f0a 0%, #0d0b09 40%, #0a0f15 100%);
  border-bottom: 1px solid var(--border);
}
.hero-texture {
  position: absolute; inset: 0;
  background-image:
    radial-gradient(circle at 20% 50%, rgba(201,169,110,0.06) 0%, transparent 50%),
    radial-gradient(circle at 80% 20%, rgba(139,26,26,0.08) 0%, transparent 40%),
    url("data:image/svg+xml,%3Csvg width='60' height='60' viewBox='0 0 60 60' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cg fill='%23c9a96e' fill-opacity='0.02'%3E%3Cpath d='M36 34v-4h-2v4h-4v2h4v4h2v-4h4v-2h-4zm0-30V0h-2v4h-4v2h4v4h2V6h4V4h-4zM6 34v-4H4v4H0v2h4v4h2v-4h4v-2H6zM6 4V0H4v4H0v2h4v4h2V6h4V4H6z'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E");
}
.hero-content { position: relative; z-index: 2; max-width: 600px; }
.hero-eyebrow { font-family: 'Cinzel', serif; font-size: 0.75rem; letter-spacing: 0.25em; text-transform: uppercase; color: var(--sepia); margin-bottom: 1rem; }
.hero-title {
  font-family: 'Cinzel', serif; font-size: clamp(3rem, 6vw, 5rem);
  line-height: 1.05; color: var(--parchment); margin-bottom: 1.5rem;
  text-shadow: 0 0 60px rgba(201,169,110,0.2);
}
.hero-title em { color: var(--sepia); font-family: 'Cormorant Garamond', serif; font-size: 1.1em; }
.hero-body { font-size: 1.1rem; color: var(--parchment-dark); max-width: 600px; margin-bottom: 2rem; line-height: 1.7; }
.hero-art { position: absolute; right: 6%; top: 50%; transform: translateY(-50%); z-index: 1; }
.floating-rune {
  font-size: 5rem; color: var(--sepia); opacity: 0.12;
  position: absolute; animation: drift 8s ease-in-out infinite;
}
.floating-rune.two { font-size: 3rem; top: 80px; left: 60px; animation-delay: -3s; animation-duration: 11s; }
.floating-rune.three { font-size: 4rem; top: -40px; left: 100px; animation-delay: -6s; animation-duration: 9s; }
@keyframes drift { 0%, 100% { transform: translateY(0) rotate(0deg); } 50% { transform: translateY(-20px) rotate(5deg); } }

/* ===== LANDING AUTH — Batch 76 ===== */
.landing-auth-section { padding: 3rem 2rem; }
.landing-auth-inner {
  display: flex; gap: 0; max-width: 960px; margin: 0 auto;
  min-height: 520px; align-items: stretch;
}
.landing-auth-panel {
  flex: 1; padding: 2rem 2.5rem; display: flex; flex-direction: column;
}
.landing-auth-heading {
  font-family: 'Cinzel', serif; font-size: 1.4rem; color: var(--parchment);
  letter-spacing: 0.06em; margin-bottom: 1.5rem;
}
.landing-auth-divider {
  width: 1px; background: linear-gradient(180deg, transparent, var(--sepia), transparent);
  flex-shrink: 0;
}
@media (max-width: 768px) {
  .landing-auth-inner { flex-direction: column; }
  .landing-auth-divider { width: 100%; height: 1px; background: linear-gradient(90deg, transparent, var(--sepia), transparent); }
  .landing-auth-panel { padding: 2rem 1rem; }
}

/* ===== AUTHOR CONTROLS ===== */
/* Batch 211 — .author-controls rule retired. Its only consumers (#authorControls,
   #loreKeeperControls) were empty permanently-hidden divs, removed from index.html
   this batch along with their inert updateUI toggles. Selector kept here as a comment
   for provenance; no element carries the class any longer.
   .author-controls { display: flex; gap: 1rem; margin-bottom: 2rem; flex-wrap: wrap; } */

/* ===== FEED LAYOUT ===== */
.feed-layout { display: grid; grid-template-columns: 1fr 320px; gap: 2rem; }
/* Batch 210 Item 2 — the 1fr feed-main track has implicit min-width:auto, so wide
   children (YouTube embeds, full-bleed post images) force the track wider than its
   intended share. The centered .feed-header then centers across that over-wide track
   and lands left of the posts. min-width:0 lets the track hold its computed width so
   the header centers over the actual feed column, aligned with the posts beneath. */
.feed-main { min-width: 0; }
@media (max-width: 1024px) { .feed-layout { grid-template-columns: 1fr; } .feed-sidebar { position: static; max-height: none; overflow-y: visible; } }
/* Batch 203 R4 — Community feed header centered + stacked: the "Community Feed"
   label sits centered at the top of the main feed column, the filter chips
   directly beneath it (also centered). The Load New Posts control already sits
   centered below this header, then the posts. Layout-only change. */
.feed-header { display: flex; flex-direction: column; align-items: center; text-align: center; margin-bottom: 1.5rem; gap: 1rem; }
.feed-header .section-title { margin-bottom: 0; }
.feed-filters { display: flex; gap: 0.5rem; flex-wrap: wrap; justify-content: center; }
.filter-btn {
  background: none; border: 1px solid var(--border); cursor: pointer;
  color: var(--text-muted); font-family: 'Cinzel', serif; font-size: 0.72rem;
  padding: 0.35rem 0.8rem; border-radius: 50px; letter-spacing: 0.06em;
  text-transform: uppercase; transition: all 0.2s;
}
.filter-btn:hover, .filter-btn.active { background: rgba(201,169,110,0.12); color: var(--sepia); border-color: var(--border-strong); }

/* ===== POST CARDS ===== */
.post-card {
  background: var(--ash);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  margin-bottom: 1.25rem;
  transition: border-color 0.2s, transform 0.2s;
  cursor: pointer;
  position: relative;
  max-width: 620px;
}
.post-card:hover { border-color: var(--border-strong); }
.post-card-image-wrap { max-height: 600px; overflow: hidden; border-radius: var(--radius-lg) var(--radius-lg) 0 0; display: flex; align-items: center; }
.post-card-image { width: 100%; display: block; }
.post-card-body { padding: 1.5rem; }
.post-meta { display: flex; gap: 0.75rem; align-items: center; margin-bottom: 0.75rem; flex-wrap: wrap; }
.post-tag {
  font-family: 'Cinzel', serif; font-size: 0.65rem; letter-spacing: 0.12em;
  text-transform: uppercase; padding: 0.2rem 0.6rem; border-radius: 50px;
  border: 1px solid;
}
.post-tag.commentary { color: var(--sepia); border-color: var(--sepia); }
.post-tag.lore { color: #6baed6; border-color: #6baed6; }
.post-tag.forum { color: #74c476; border-color: #74c476; }
/* Batch 252 — Feed bump "Round N" chip on re-introduced debate cards. Gold-lit to
   read as new activity against the green forum tag beside it. */
.post-tag.feed-bump-badge { color: var(--gold); border-color: var(--gold); text-shadow: 0 0 6px rgba(212,175,55,0.45); }
.post-tag.announcement { color: var(--gold); border-color: var(--gold); }
.post-date { font-size: 0.85rem; color: var(--text-muted); }
.post-author-chip { font-size: 0.85rem; color: var(--text-muted); }
.post-title { font-family: 'Cinzel', serif; font-size: 1.2rem; color: var(--parchment); margin-bottom: 0.75rem; line-height: 1.3; }
.post-excerpt { color: var(--parchment-dark); font-size: 1rem; display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; }
.post-actions { display: flex; gap: 1rem; padding: 1rem 1.5rem; border-top: 1px solid var(--border); }
.action-btn {
  background: none; border: none; cursor: pointer; color: var(--text-muted);
  font-size: 0.9rem; display: flex; align-items: center; gap: 0.35rem;
  transition: color 0.2s; font-family: 'Crimson Pro', serif;
}
.action-btn:hover { color: var(--sepia); }
.action-btn.liked { color: var(--crimson-light); }
.audio-player { padding: 0 1.5rem 1rem; }
.audio-player audio { width: 100%; height: 36px; filter: sepia(0.5); }

/* ===== SIDEBAR ===== */
.sidebar-card { background: var(--ash); border: 1px solid var(--border); border-radius: var(--radius-lg); padding: 1.25rem; margin-bottom: 1.25rem; }
.sidebar-title { font-family: 'Cinzel', serif; font-size: 0.85rem; letter-spacing: 0.1em; text-transform: uppercase; color: var(--sepia); margin-bottom: 1rem; padding-bottom: 0.5rem; border-bottom: 1px solid var(--border); }
.tier-summary { display: flex; flex-direction: column; gap: 0.75rem; }
.tier-row { display: flex; gap: 0.75rem; align-items: flex-start; cursor: pointer; padding: 0.5rem; border-radius: var(--radius); transition: background 0.2s; }
.tier-row:hover { background: rgba(201,169,110,0.06); }
.tier-dot { width: 10px; height: 10px; border-radius: 50%; margin-top: 0.4rem; flex-shrink: 0; }
.tier-dot.reader { background: #74c476; }
.tier-dot.scribe { background: #6baed6; }
.tier-dot.lore { background: var(--gold); }
.tier-row strong { font-family: 'Cinzel', serif; font-size: 0.8rem; letter-spacing: 0.04em; display: block; color: var(--parchment); }
.tier-row p { font-size: 0.85rem; color: var(--text-muted); margin: 0; }
.member-chip { display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0.6rem; font-size: 0.9rem; }
.member-avatar-sm { width: 28px; height: 28px; border-radius: 50%; background: var(--ash-mid); display: flex; align-items: center; justify-content: center; font-size: 0.7rem; font-family: 'Cinzel', serif; color: var(--sepia); border: 1px solid var(--border); }
.member-name { color: var(--parchment-dark); }

/* Batch 135 Rev 3 — Sticky community sidebar with independent scroll */
/* BATCH 290 (C4) — NESTED SCROLL CONTAINERS, resolved by removing the OUTER one.
   This rule previously carried max-height + overflow-y:auto + overscroll-behavior,
   making the sidebar itself a scroll container WRAPPING the cards inside it, each
   of which is its own capped scroller (#activeMemberList 440px, Autarch's Choice
   and Announcements built at 200/320px by their renderers). Two scrollers, one
   wheel: WebKit targets the innermost element under the cursor and chains outward
   only at its boundary, so Safari behaved; Blink's latching heuristic binds the
   gesture to the first viable ancestor it accepts, and this sticky, viewport-
   capped, contain-marked box was exactly that — so in Chrome the wheel was
   consumed here and the inner card never received it. The scrollbar still painted
   and still DRAGGED (dragging is direct manipulation, not a wheel event), which
   is precisely the symptom reported: "I have to manually click the scroll bar."
   The sidebar no longer scrolls independently — the PAGE scrolls it, and each
   card owns its own wheel. #activeMemberList is the proof case: the one nested
   scroller in this sidebar that was never reported broken is also the only one
   that declared its own scrollbar explicitly (styles.css:438-440), and the two
   cards that WERE broken did not. position:sticky with align-self:start does
   NOT require a height cap, so the sticky behaviour is unaffected; the
   ::-webkit-scrollbar rules below are left in place but simply never render now.
   overscroll-behavior:contain is retained ONLY on the inner cards, where it
   correctly stops boundary chaining rather than competing for the gesture.
   The mobile rule at the @media (max-width:1024px) breakpoint already set
   max-height:none / overflow-y:visible — this makes the desktop rule agree with
   it rather than contradict it. */
/* BATCH 291 (S-1) — SIDEBAR SCROLL RESTORED. Batch 290's C4 removed max-height,
   overflow-y and overscroll-behavior from this rule to stop Blink consuming the
   wheel before it reached the inner cards. It worked, and it cost the sidebar its
   OWN scroller: the draggable scrollbar the Autarch used vanished in every browser
   (the ::-webkit-scrollbar rules below painted nothing without an overflow), and
   Safari — which had never had the wheel fault, because WebKit targets the
   innermost scroller under the cursor — lost a working scroll as collateral. A
   Chrome-only fault was traded for an all-browser one. That is a 2.33 improvement-
   clause breach and it is corrected here.
   The scroller returns; the LATCH does not. Blink's wheel-latching heuristic binds
   a gesture to an ancestor scroll container when that container declares
   overscroll-behavior — the property is an explicit statement that the box owns its
   gesture boundary, and Blink honours it by claiming the gesture at the outermost
   box that declares it. overscroll-behavior is therefore the one property NOT
   restored here. Without it Blink falls back to hit-test order and delivers the
   wheel to the innermost scroller under the cursor — the same targeting WebKit has
   always used — so the inner cards keep their wheel and the sidebar keeps its bar.
   overscroll-behavior:contain remains on #acScrollBox / #annScrollBox / #activeMemberList,
   where it correctly stops boundary chaining rather than competing for the gesture.
   max-height is viewport-relative against top:5rem so the sticky box ends flush with
   the viewport rather than overshooting it. The @media (max-width:1024px) rule at
   line 353 still sets max-height:none / overflow-y:visible and is untouched: mobile
   stacks the sidebar into normal page flow and must not scroll independently.
   *** BATCH 292 CORRECTION (R1): the sentence above was TRUE ABOUT THE TEXT and
   FALSE ABOUT THE EFFECT. The mobile rule's text was indeed never edited, but 291
   placed competing declarations in a later, equal-specificity rule, so the mobile
   rule stopped WINNING. "Untouched" read as "mobile is safe"; mobile was not safe.
   291's harness asserted the mobile rule's text was present and passed — proving
   nothing about which rule the browser applies. That is the exact failure 2.35
   names. Corrected below by scoping the desktop declarations to (min-width:1025px)
   so the two rules are mutually exclusive and order-independent. *** */
/* BATCH 292 (R1) — DESKTOP SCROLLER SCOPED BY MEDIA QUERY, NOT BY SOURCE ORDER.
   Batch 291's S-1 restored max-height + overflow-y to the bare .feed-sidebar
   selector here at line ~486. The @media (max-width:1024px) override that unsets
   them sits EARLIER in the file (line 353). Both selectors are a single class —
   specificity (0,1,0) — and a media query contributes NO specificity, so at equal
   specificity the LATER rule wins: this rule beat the mobile override and phones
   and tablets computed position:sticky / max-height:calc(...) / overflow-y:auto
   when they require static / none / visible. The sidebar became a capped inner
   scroller inside the mobile stack, which is the exact shape Batch 290's C4 set
   out to remove. The ordering hazard is older than 291 and was DORMANT only
   because 290's rule declared no max-height or overflow at all; 291 reintroduced
   those declarations and activated it. Proven by a cascade resolver run against
   this stylesheet at 1440 / 1024 / 390px: six failures at the two lower widths.
   Fixed at the root rather than by moving lines: the scroller now lives in its
   own (min-width:1025px) block, the exact complement of the mobile breakpoint, so
   the two can never both apply and RE-ORDERING THE FILE CANNOT BREAK IT AGAIN.
   The unconditional properties below stay unconditional — position:sticky is
   correctly overridden to static by the mobile rule (that override is order-
   independent for position because the mobile rule is the only other declarer of
   it... which is precisely the fragility being removed here, so sticky is scoped
   too). Only scrollbar cosmetics remain global; they are inert without overflow. */
.feed-sidebar {
  align-self: start;
  scrollbar-width: thin; scrollbar-color: var(--border) transparent;
}
@media (min-width: 1025px) {
  .feed-sidebar {
    position: sticky; top: 5rem;
    max-height: calc(100vh - 6rem); overflow-y: auto;
  }
}
/* BATCH 293 (A1) — ONE SIDEBAR SCROLLER (Autarch ruling: hover anywhere over the
   sidebar, the whole column scrolls as one unit). The rule above already makes
   .feed-sidebar the desktop scroller — that was never the fault. The fault was that
   the three inner cards (#acScrollBox, #annScrollBox, #activeMemberList) ALSO
   declared overflow-y:auto, so the sidebar wrapped three capped scrollers: two-
   scroller nesting, one wheel. WebKit targets the innermost element under the
   cursor; Blink latches the gesture to the first viable ancestor — so on Chrome the
   inner bar painted and DRAGGED while the wheel did nothing, the drag-only symptom
   reported across Batches 290-292. Every prior attempt adjusted the OUTER rule and
   left the nesting; this one removes the nesting at its source. The inner boxes are
   changed from overflow-y:auto to overflow:hidden (in app.js for the two rendered
   boxes, in index.html for #activeMemberList), so they are no longer scrollers —
   .feed-sidebar is the SOLE scroller and owns the wheel everywhere. The cards keep
   their max-height, so the expand/collapse control is UNTOUCHED: _acMaxH still sets
   each card to 200/320/380px or none, the collapsed/default window now CLIPS instead
   of scrolling-within, and the extra posts are revealed by the +/- expand button —
   while the sidebar scrolls the whole column past it. overscroll-behavior:contain is
   dropped with the inner overflow (inert without a scroll container) so nothing
   declares gesture ownership that could re-arm the Blink latch. */
.feed-sidebar::-webkit-scrollbar { width: 6px; }
.feed-sidebar::-webkit-scrollbar-track { background: transparent; }
.feed-sidebar::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
#activeMemberList::-webkit-scrollbar { width: 5px; }
#activeMemberList::-webkit-scrollbar-track { background: transparent; }
#activeMemberList::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }

/* Batch 70 — Featured Book + Book Grid + Book Filters CSS removed (books pipeline dead). */

/* ===== MODALS ===== */
.modal-overlay {
  position: fixed; inset: 0; z-index: 1000;
  background: rgba(0,0,0,0.7); backdrop-filter: blur(4px);
  display: flex; align-items: center; justify-content: center;
  padding: 1rem; animation: fadeIn 0.2s ease;
  overflow: hidden;
}
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
.modal-box {
  background: var(--ash); border: 1px solid var(--border-strong);
  border-radius: var(--radius-lg); padding: 2rem;
  width: 100%; max-width: 480px; max-height: 90vh; overflow-y: auto;
  overscroll-behavior: contain;
  position: relative; animation: slideUp 0.25s ease;
  box-shadow: var(--shadow-heavy);
}
.modal-box.wide { max-width: 720px; }
@keyframes slideUp { from { transform: translateY(24px); opacity: 0; } to { transform: translateY(0); opacity: 1; } }
.modal-close {
  position: absolute; top: 1rem; right: 1rem;
  background: none; border: none; color: var(--text-muted); cursor: pointer;
  font-size: 1rem; transition: color 0.2s;
}
.modal-close:hover { color: var(--parchment); }
.modal-title { font-family: 'Cinzel', serif; font-size: 1.4rem; color: var(--sepia); margin-bottom: 1.5rem; }

/* ===== FORMS ===== */
.form-group { margin-bottom: 1.25rem; }
.form-group label { display: block; font-family: 'Cinzel', serif; font-size: 0.75rem; letter-spacing: 0.08em; text-transform: uppercase; color: var(--text-muted); margin-bottom: 0.4rem; }
.form-group input, .form-group textarea, .form-group select {
  width: 100%; background: var(--ash-mid); border: 1px solid var(--border);
  color: var(--parchment); padding: 0.65rem 0.9rem; border-radius: var(--radius);
  font-family: 'Crimson Pro', serif; font-size: 1rem; outline: none;
  transition: border-color 0.2s;
}
.form-group input:focus, .form-group textarea:focus, .form-group select:focus { border-color: var(--sepia); }
.form-group input::placeholder, .form-group textarea::placeholder { color: var(--text-muted); }
.form-group select option { background: var(--ash-mid); }
.form-note { font-size: 0.82rem; color: var(--text-muted); margin-top: 0.75rem; line-height: 1.5; }

/* ===== AUTH TABS ===== */
.auth-tabs { display: flex; gap: 0; margin-bottom: 1.5rem; border-bottom: 1px solid var(--border); }
.auth-tab {
  background: none; border: none; cursor: pointer;
  font-family: 'Cinzel', serif; font-size: 0.85rem; letter-spacing: 0.08em;
  color: var(--text-muted); padding: 0.6rem 1.5rem;
  text-transform: uppercase; border-bottom: 2px solid transparent;
  margin-bottom: -1px; transition: all 0.2s;
}
.auth-tab.active { color: var(--sepia); border-bottom-color: var(--sepia); }

/* ===== POST MODAL ===== */
#postModalContent .post-full-title { font-family: 'Cinzel', serif; font-size: 1.6rem; color: var(--parchment); margin: 1rem 0; line-height: 1.3; }
#postModalContent .post-full-body { font-size: 1.1rem; line-height: 1.8; color: var(--parchment-dark); margin-bottom: 1.5rem; white-space: pre-wrap; }
#postModalContent .post-full-img { width: 100%; border-radius: var(--radius); margin-bottom: 1.5rem; }
.comments-section { border-top: 1px solid var(--border); padding-top: 1.5rem; }
.comments-title { font-family: 'Cinzel', serif; font-size: 1rem; color: var(--sepia); margin-bottom: 1rem; }
.comment { padding: 0.75rem; border-left: 2px solid var(--border); margin-bottom: 0.75rem; }
.comment-author { font-family: 'Cinzel', serif; font-size: 0.78rem; color: var(--sepia); margin-bottom: 0.25rem; }
.comment-body { white-space: pre-wrap; word-wrap: break-word; }
.comment-input-area { display: flex; gap: 0.75rem; margin-top: 0.25rem; }
.comment-input-area input { flex: 1; background: var(--ash-mid); border: 1px solid var(--border); color: var(--parchment); padding: 0.6rem 0.9rem; border-radius: var(--radius); font-family: 'Crimson Pro', serif; font-size: 1rem; outline: none; }
.comment-input-area input:focus { border-color: var(--sepia); }

/* Batch 144 Rev 2 — Opaque sticky bars. No sticky section is see-through. */
.sticky-modal-bar {
  background: var(--ash) !important;
  border-top: 1px solid var(--border);
  /* Batch 208 Item 1 — bottom-edge seal. A sticky bottom:0 bar pins at its scroll
     container's CONTENT-box edge; when that container carries bottom padding the
     bar sits above the painted bottom and thread content scrolling in the padding
     strip bleeds through beneath the bar. padding-bottom alone (shipped prior) only
     pads the bar's content UP — it never extends the fill DOWN past the sticky
     anchor. The negative margin drops the bar's painted box by the same amount so
     its opaque --ash fill covers the strip; padding+margin cancel in layout, so the
     visual footprint is unchanged. Scroll-axis overflow on overflow-y:auto parents
     is scrollable, not clipped, so the fill paints on every surface (notif-detail
     panel, comment thread modal, gallery overlay, sidebar viewers). */
  padding-bottom: 2.5rem;
  margin-bottom: -2.5rem;
  z-index: 2;
  transform: translateZ(0);
}
.sticky-modal-header {
  background: var(--ash) !important;
  z-index: 2;
  transform: translateZ(0);
}

/* Batch 251 — Partners-panel sticky control bar (Rule 2.28: opaque, never
   see-through, NO bleed above or below). One bar on all three tabs holding
   the Pending/Outreach/Accepted labels + the action row (the Outreach tab's
   row also carries the email search + Filter). The "Partners" title stays
   OUTSIDE and scrolls away (Autarch ruling).
   TOP seal: sticky top:0 pins at the scroll container's CONTENT-box edge
   (the Batch 208 finding), so .modal-box's 2rem padding-top would remain a
   strip above the pinned bar where listings show through. Fix at the source:
   the panel's modal drops its padding-top and the title carries it instead —
   rest layout is pixel-identical, and the pinned bar sits flush at the true
   scrollport top with NOTHING visible above it (scrolled content clips at
   the scrollport edge).
   BOTTOM seal: the 1rem gap below the bar is transparent margin; paint the
   band with an --ash box-shadow (the Batch 229 F2 technique) so no listing
   shows through it. Own compositing layer via translateZ(0) (Batch 150) and
   z-index above the list so stacking resolves with the bar on top; the
   filter dropdown (.post-menu, position:absolute) escapes the bar and paints
   over the list within the same raised context. */
#partnersModal .modal-box { padding-top: 0; }
#partnersModal .modal-title { padding-top: 2rem; }
.partners-sticky-bar {
  position: sticky; top: 0; z-index: 3;
  background: var(--ash) !important;
  padding-top: 0.75rem;
  border-bottom: 1px solid var(--border);
  padding-bottom: 0.75rem;
  margin-bottom: 1rem;
  box-shadow: 0 1rem 0 var(--ash);
  transform: translateZ(0);
}

/* ===== LIGHTBOX ===== */
.lightbox-content { max-width: 90vw; max-height: 90vh; text-align: center; position: relative; }
.lightbox-content img { max-width: 100%; max-height: 85vh; border-radius: var(--radius); display: block; margin: 0 auto; }
.lightbox-content p { color: var(--parchment-dark); margin-top: 0.75rem; font-size: 0.95rem; }

/* ===== MESSAGES ===== */

/* ===== ADMIN PANEL ===== */
.admin-section { margin-bottom: 2rem; }
.admin-section h3 { font-family: 'Cinzel', serif; font-size: 1rem; color: var(--sepia); margin-bottom: 1rem; padding-bottom: 0.5rem; border-bottom: 1px solid var(--border); }
/* Batch 36 — Removed duplicate .admin-user-row definition (predated Batch 29). Active definition at line ~730. */
.stat-grid { display: grid; grid-template-columns: repeat(3,1fr); gap: 1rem; }
.stat-box { background: var(--ash-mid); border: 1px solid var(--border); border-radius: var(--radius); padding: 1rem; text-align: center; }

/* ===== PROFILE ===== */
.profile-avatar { width: 60px; height: 60px; border-radius: 50%; background: linear-gradient(135deg, var(--sepia-dark), var(--sepia)); display: flex; align-items: center; justify-content: center; font-family: 'Cinzel', serif; font-size: 1.4rem; color: var(--ink); border: 2px solid var(--border-strong); }
/* Batch 59 Fix — .profile-tier-badge removed: zero renderers after Batch 59 Rev 8. */
.profile-stats { display: flex; gap: 1.5rem; margin-bottom: 1.5rem; }
.profile-stat { text-align: center; }

/* ===== FORUM ===== */
.forum-card {
  background: var(--ash); border: 1px solid var(--border); border-radius: var(--radius-lg);
  margin-bottom: 1rem; padding: 1.25rem; cursor: pointer;
  transition: border-color 0.2s, transform 0.2s;
}
.forum-card:hover { border-color: var(--border-strong); }
.forum-card-lore {
  background: rgba(180,140,20,0.15);
  border-color: rgba(180,140,20,0.4);
}
.forum-card-lore:hover { border-color: rgba(180,140,20,0.7); }

/* Batch 167 — Forum kind-filter tab bar: sticky + fully opaque (Rule 2.28).
   At scroll-top it sits in its natural position below the section header; as the
   page scrolls it pins flush beneath the sticky site-header (.header-inner is
   72px tall; the header sits at z-index:100 and covers the seam). Requirements
   per Rule 2.28's accumulated clarifications:
     - Opaque solid background matching the page surface (--ink, the body bg),
       NOT a translucent or darker strip (Batch 146). No forum shows through it.
     - Own compositing layer via transform:translateZ(0) so scrolled content
       cannot bleed through even when the color is technically opaque (Batch 150).
     - The forum section carries position:relative;z-index:1 (below) so stacking
       resolves with the bar definitively above scrolled content (Batch 156).
   Content scrolling BEHIND the bar is intended (matches the site-header behavior);
   the bar itself and its edges are never see-through. */
.forum-kind-tabs {
  display: flex; gap: 0; flex-wrap: wrap;
  position: sticky; top: 72px; z-index: 2;
  background: var(--ink);
  border-bottom: 1px solid var(--border);
  padding-bottom: 0; margin-bottom: 1.5rem;
  /* Batch 229 F2 — same sticky-bleed class as Batch 228 R1: the transparent
     margin-bottom:1.5rem could let a card scroll through beneath the bar. Here the
     border-bottom already masks it visually, and folding the margin into padding
     would push the border 1.5rem off the tabs (a design change). Instead paint the
     margin band with an --ink box-shadow so nothing can show through, while keeping
     the border tight to the tabs and total spacing unchanged. */
  box-shadow: 0 1.5rem 0 var(--ink);
  transform: translateZ(0);
}
/* Stacking context for the forum view so scrolled cards resolve below the
   sticky tab bar's z-index (Batch 156). */
#forumSection { position: relative; z-index: 1; }

/* ===== SHOP ===== */
.shop-card { background: var(--ash); border: 1px solid var(--border); border-radius: var(--radius-lg); overflow: hidden; transition: transform 0.2s; }
.shop-card:hover { transform: translateY(-4px); }
.shop-img { width: 100%; aspect-ratio: 1; object-fit: cover; }
.shop-info { padding: 1rem; }
.shop-name { font-family: 'Cinzel', serif; font-size: 0.95rem; color: var(--parchment); margin-bottom: 0.25rem; }
.shop-price { color: var(--sepia); font-size: 1rem; margin-bottom: 0.75rem; }

/* ===== FOOTER ===== */
.site-footer { border-top: 1px solid var(--border); padding: 3rem 2rem 1rem; margin-top: 4rem; }
.footer-inner { max-width: 1280px; margin: 0 auto; display: flex; justify-content: space-between; align-items: center; gap: 2rem; flex-wrap: wrap; padding-bottom: 2rem; }
.footer-brand { display: flex; align-items: center; gap: 1rem; }
.footer-brand p { color: var(--text-muted); font-size: 0.95rem; }
.footer-links { display: flex; gap: 1rem; flex-wrap: wrap; }
.footer-links button { background: none; border: none; color: var(--text-muted); cursor: pointer; font-family: 'Cinzel', serif; font-size: 0.78rem; letter-spacing: 0.06em; text-transform: uppercase; transition: color 0.2s; }
.footer-links button:hover { color: var(--sepia); }
.footer-bottom { max-width: 1280px; margin: 0 auto; text-align: center; color: var(--text-muted); font-size: 0.82rem; border-top: 1px solid var(--border); padding-top: 1rem; }

/* ===== TOAST ===== */
.toast {
  position: fixed; bottom: 2rem; right: 2rem; z-index: 9999;
  background: var(--ash); border: 1px solid var(--sepia);
  border-radius: var(--radius-lg); padding: 0.85rem 1.5rem;
  font-family: 'Cinzel', serif; font-size: 0.85rem; color: var(--parchment);
  box-shadow: var(--shadow); animation: slideIn 0.3s ease;
  max-width: 320px;
}
.toast.error { border-color: var(--crimson); }
@keyframes slideIn { from { transform: translateX(100px); opacity: 0; } to { transform: translateX(0); opacity: 1; } }

/* Batch 187 R5 — center-screen toast (e.g. "Duplicates Cleared"). Lands in the
   middle of the viewport, distinct from the corner .toast. Non-interactive. */
.toast-center {
  position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%);
  z-index: 10001; pointer-events: none;
  background: var(--ash); border: 1px solid var(--sepia);
  border-radius: var(--radius-lg); padding: 1.1rem 2.2rem;
  font-family: 'Cinzel', serif; font-size: 1.05rem; letter-spacing: 0.04em;
  color: var(--parchment); box-shadow: var(--shadow);
  animation: centerToastIn 0.25s ease;
}
@keyframes centerToastIn { from { transform: translate(-50%, -50%) scale(0.92); opacity: 0; } to { transform: translate(-50%, -50%) scale(1); opacity: 1; } }

/* Batch 195 R10 — verdict-confirmation toast. Centered like .toast-center but at
   z-index 10002 so it sits ABOVE the dossier/report modals (10000) and their
   overlays (10001) — previously verdict toasts used the corner .toast (9999) and
   were hidden behind the open report/dossier window. Sepia by default; the
   .guilty modifier tints crimson and .acquit tints green so the admin reads the
   outcome at a glance. Non-interactive. */
.toast-verdict {
  position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%);
  z-index: 10002; pointer-events: none;
  background: var(--ash); border: 1px solid var(--sepia);
  border-radius: var(--radius-lg); padding: 1.1rem 2.2rem;
  font-family: 'Cinzel', serif; font-size: 1.05rem; letter-spacing: 0.04em;
  color: var(--parchment); box-shadow: var(--shadow);
  animation: centerToastIn 0.25s ease; text-align: center; max-width: 80vw;
}
.toast-verdict.guilty { border-color: var(--crimson); color: #e06060; }
.toast-verdict.acquit { border-color: #60b870; color: #60b870; }

/* Batch 218 — signup-rejection variant of the center toast (Autarch directive:
   signup validation rejections land in the middle of the page). */
.toast-center.error { border-color: var(--crimson); color: #e06060; }

/* Batch 218 — STICKY CENTER TOAST (Autarch directive). Persists until the user
   clicks the X — no auto-dismiss. Used for the post-payment access-code
   instruction. Interactive (pointer-events on, unlike .toast-center); z-index
   10003 sits above the verdict toast (10002) and every modal/overlay. */
.toast-sticky {
  position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%);
  z-index: 10003;
  background: var(--ash); border: 1px solid var(--sepia);
  border-radius: var(--radius-lg); padding: 1.25rem 2.6rem 1.25rem 1.6rem;
  font-family: 'Cinzel', serif; font-size: 1rem; letter-spacing: 0.04em;
  color: var(--parchment); box-shadow: var(--shadow);
  animation: centerToastIn 0.25s ease; text-align: center;
  max-width: min(92vw, 520px); line-height: 1.6;
}
.toast-sticky.error { border-color: var(--crimson); color: #e06060; }
.toast-sticky-x {
  position: absolute; top: 4px; right: 8px;
  background: none; border: none; cursor: pointer;
  color: var(--text-muted); font-size: 1.05rem; line-height: 1;
  padding: 5px; font-family: inherit;
}
.toast-sticky-x:hover { color: var(--parchment); }

/* Batch 218 — GOLD SPINNER (Autarch directive). The spinning circle of gold shown
   while the membership payment is confirmed and processed at registration. */
.gold-spinner {
  width: 46px; height: 46px; margin: 0 auto;
  border: 4px solid var(--ash-mid);
  border-top-color: var(--gold);
  border-radius: 50%;
  animation: dmsGoldSpin 0.9s linear infinite;
}
@keyframes dmsGoldSpin { to { transform: rotate(360deg); } }

/* ============================================================
   Batch 225 — CANCELLATION LIFECYCLE (Trouble Shoot admin pipeline)
   R2 .ts-context (landing textarea); R3 row controls; R4 LEA + gray gating;
   R9 cleared search; R10 in-window refresh button (discover-feed spin).
   ============================================================ */
/* R2 — landing-page context textarea, matching the ts form fields. */
.ts-context {
  width: 100%; box-sizing: border-box; resize: vertical; min-height: 84px;
  background: var(--ash-mid); color: var(--parchment);
  border: 1px solid var(--border); border-radius: var(--radius);
  padding: 0.6rem; font-family: Georgia, serif; font-size: 0.9rem;
}
.ts-context:focus { outline: none; border-color: var(--sepia); }

/* R10 — in-window refresh button. Same circular glyph as trending; spins on the
   discover-feed gold pattern while refreshing. */
.cancel-refresh-btn {
  display: inline-flex; align-items: center; justify-content: center;
  width: 30px; height: 30px; flex-shrink: 0;
  border: 1px solid var(--sepia); border-radius: 50%;
  background: transparent; color: var(--sepia);
  cursor: pointer; padding: 0; font-size: 1rem; line-height: 1;
  transition: color 0.15s, border-color 0.15s;
}
.cancel-refresh-btn:hover { color: var(--gold); border-color: var(--gold); }
.cancel-refresh-icon { display: inline-block; }
.cancel-refresh-btn.spinning .cancel-refresh-icon { animation: dmsGoldSpin 0.9s linear infinite; }

/* R3 — Confirm button: green-on-click per spec. */
.cancel-confirm-btn {
  padding: 0.35rem 0.7rem; border-radius: var(--radius); cursor: pointer;
  border: 1px solid var(--border-strong); background: var(--ash-mid);
  color: var(--parchment); font-family: Cinzel, serif; font-size: 0.72rem;
  letter-spacing: 0.04em; transition: background 0.15s, color 0.15s, border-color 0.15s;
}
.cancel-confirm-btn:hover { border-color: #74c476; color: #74c476; }
.cancel-confirm-btn.sent {
  background: #2e7d32; border-color: #74c476; color: #eafbe7;
}

/* Finding A fix — Mark Reply / Reply Received control. Neutral until set, then
   green latch (like Confirm's sent state) to signal the gray has been lifted. */
.cancel-reply-btn {
  padding: 0.35rem 0.7rem; border-radius: var(--radius); cursor: pointer;
  border: 1px solid var(--border-strong); background: var(--ash-mid);
  color: var(--parchment); font-family: Cinzel, serif; font-size: 0.72rem;
  letter-spacing: 0.04em; transition: background 0.15s, color 0.15s, border-color 0.15s;
}
.cancel-reply-btn:hover { border-color: #74c476; color: #74c476; }
.cancel-reply-btn.set {
  background: #2e7d32; border-color: #74c476; color: #eafbe7; cursor: default;
}

/* R3 — Cancel Account / Delete Request: stacked two-line text (Image 2 style).
   Gold-when-clickable; grayed (via inline opacity) until reply/LEA lifts it. */
.cancel-action-btn {
  padding: 0.3rem 0.6rem; border-radius: var(--radius); cursor: pointer;
  border: 1px solid var(--border-strong); background: var(--ash-mid);
  color: var(--parchment); font-family: Cinzel, serif; font-size: 0.68rem;
  line-height: 1.1; text-align: center; letter-spacing: 0.03em;
  transition: background 0.15s, color 0.15s, border-color 0.15s;
}
.cancel-action-btn:hover { background: var(--gold); border-color: var(--gold); color: #1a1a14; }

/* R4 — LEA checkbox label. */
.cancel-lea {
  display: inline-flex; align-items: center; gap: 0.3rem;
  font-family: Cinzel, serif; font-size: 0.68rem; letter-spacing: 0.04em;
  color: var(--text-muted); cursor: pointer; user-select: none;
}
.cancel-lea input { cursor: pointer; }

/* R3 — Context disclosure box. */
.cancel-context-box {
  margin-top: 0.5rem; padding: 0.6rem; border-radius: var(--radius);
  background: var(--ash-mid); border: 1px solid var(--border);
  color: var(--parchment-dark); font-size: 0.82rem; line-height: 1.5;
  white-space: pre-wrap; word-break: break-word;
}

/* R9 — Cleared window search bar (userLibrary-style). */
.cleared-search {
  width: 100%; box-sizing: border-box; margin-top: 0.5rem;
  background: var(--ash-mid); color: var(--parchment);
  border: 1px solid var(--border); border-radius: var(--radius);
  padding: 0.55rem 0.7rem; font-family: Georgia, serif; font-size: 0.9rem;
}
.cleared-search:focus { outline: none; border-color: var(--sepia); }

/* ===== RESPONSIVE ===== */
@media (max-width: 768px) {
  .main-nav { display: none; }
  .hero { padding: 3rem 2rem; min-height: 440px; }
  .hero-art { display: none; }
  .hero-title { font-size: 2.5rem; }
  .book-grid { grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); }
  .stat-grid { grid-template-columns: repeat(2,1fr); }
  .modal-box { padding: 1.5rem; }
  /* Batch 251 F1 — the desktop rule relocates the Partners modal's
     padding-top onto the title at 2rem (the sticky-bar top seal). On mobile
     the box padding is 1.5rem, so 2rem sat the title 0.5rem lower than
     pre-251. Match it here; equal specificity (1,0,1), later in source, so
     this wins at <=768px only. Desktop pixel-identical; the seal
     (#partnersModal .modal-box padding-top:0) is untouched on both. */
  #partnersModal .modal-title { padding-top: 1.5rem; }
}

/* ===== SCROLLBAR ===== */
::-webkit-scrollbar { width: 6px; }
::-webkit-scrollbar-track { background: var(--ink); }
::-webkit-scrollbar-thumb { background: var(--ash-light); border-radius: 3px; }
::-webkit-scrollbar-thumb:hover { background: var(--sepia-dark); }

/* ===== LOCKED OVERLAY ===== */
.locked-overlay {
  background: rgba(13,11,9,0.85); border: 1px dashed var(--border-strong);
  border-radius: var(--radius-lg); padding: 2rem; text-align: center;
  margin: 1rem 0;
}
.locked-icon { font-size: 2rem; margin-bottom: 0.75rem; }
.locked-msg { font-family: 'Cinzel', serif; font-size: 0.9rem; color: var(--sepia); margin-bottom: 1rem; }

/* ===== EMPTY STATE ===== */
.empty-state { text-align: center; padding: 3rem; color: var(--text-muted); }
.empty-icon { font-size: 3rem; display: block; margin-bottom: 1rem; opacity: 0.4; }

/* ========================================
   FULL PROFILE PAGE
   ======================================== */

.profile-page-overlay {
  position: fixed; inset: 0; z-index: 200;
  background: var(--ink);
  overflow-y: auto;
  animation: fadeIn 0.25s ease;
}

.profile-page {
  max-width: 1100px; margin: 0 auto;
  padding: 2rem 2rem 4rem;
}

.profile-page-close {
  background: none; border: 1px solid var(--border);
  color: var(--text-muted); cursor: pointer;
  font-family: 'Cinzel', serif; font-size: 0.78rem;
  letter-spacing: 0.08em; text-transform: uppercase;
  padding: 0.5rem 1rem; border-radius: var(--radius);
  margin-bottom: 2rem; display: inline-block;
  transition: all 0.2s;
}
.profile-page-close:hover { color: var(--sepia); border-color: var(--sepia); }

/* Profile Banner */
.profile-banner {
  background: linear-gradient(135deg, #1a0f0a 0%, #0d0b09 50%, #0a0f15 100%);
  border: 1px solid var(--border); border-radius: var(--radius-lg);
  padding: 2.5rem; margin-bottom: 2rem; position: relative; overflow: hidden;
}
.profile-banner::before {
  content: '✦';
  position: absolute; right: 2rem; top: 50%; transform: translateY(-50%);
  font-size: 6rem; color: var(--sepia); opacity: 0.06;
}
.profile-banner-top { display: flex; align-items: center; gap: 2rem; flex-wrap: wrap; }
.profile-avatar-large {
  width: 90px; height: 90px; border-radius: 50%;
  background: linear-gradient(135deg, var(--sepia-dark), var(--sepia));
  display: flex; align-items: center; justify-content: center;
  font-family: 'Cinzel', serif; font-size: 2.2rem; color: var(--ink);
  border: 3px solid var(--border-strong); flex-shrink: 0;
}
.profile-banner-info { flex: 1; }
.profile-page-username {
  font-family: 'Cinzel', serif; font-size: 2rem;
  margin-bottom: 0.4rem; line-height: 1.2;
}
.profile-page-meta { display: flex; gap: 1rem; flex-wrap: wrap; align-items: center; margin-top: 0.5rem; }
.profile-page-joined { font-size: 0.85rem; color: var(--text-muted); }
.profile-banner-actions { display: flex; gap: 0.75rem; margin-top: 1.5rem; flex-wrap: wrap; }

/* Profile Stats Bar */
.profile-stats-bar {
  display: flex; gap: 0; border: 1px solid var(--border);
  border-radius: var(--radius-lg); overflow: hidden; margin-bottom: 2rem;
}
.profile-stat-block {
  flex: 1; text-align: center; padding: 1.25rem 1rem;
  border-right: 1px solid var(--border); background: var(--ash);
}
.profile-stat-block:last-child { border-right: none; }
.profile-stat-block .num {
  font-family: 'Cinzel', serif; font-size: 1.6rem; color: var(--sepia);
  display: block; margin-bottom: 0.2rem;
}
.profile-stat-block .label { font-size: 0.78rem; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.08em; font-family: 'Cinzel', serif; }

.profile-card {
  background: var(--ash); border: 1px solid var(--border);
  border-radius: var(--radius-lg); padding: 1.5rem;
}
.profile-card-title {
  font-family: 'Cinzel', serif; font-size: 0.82rem; letter-spacing: 0.12em;
  text-transform: uppercase; color: var(--sepia); margin-bottom: 1rem;
  padding-bottom: 0.5rem; border-bottom: 1px solid var(--border);
}
.profile-card.full-width { grid-column: 1 / -1; }

/* Batch 37 Rev 2 — Profile two-column layout */
.profile-columns {
  display: flex; gap: 1.5rem; align-items: flex-start; margin-top: 1.5rem;
}
.profile-feed-column {
  flex: 3; min-width: 0;
  max-height: calc(100vh - 80px); overflow-y: auto;
  scrollbar-width: thin; scrollbar-color: var(--border) transparent;
}
.profile-feed-column::-webkit-scrollbar { width: 6px; }
.profile-feed-column::-webkit-scrollbar-track { background: transparent; }
.profile-feed-column::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
.profile-sidebar-column {
  flex: 2; min-width: 0;
  position: sticky; top: 1rem; max-height: calc(100vh - 80px); overflow-y: auto;
  scrollbar-width: thin; scrollbar-color: var(--border) transparent;
}
.profile-sidebar-column::-webkit-scrollbar { width: 6px; }
.profile-sidebar-column::-webkit-scrollbar-track { background: transparent; }
.profile-sidebar-column::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
.profile-sidebar-column .profile-card { margin-bottom: 1.5rem; }
@media (max-width: 700px) {
  .profile-columns { flex-direction: column; }
  .profile-feed-column { max-height: none; overflow-y: visible; }
  .profile-sidebar-column { position: static; max-height: none; overflow-y: visible; }
}

/* Profile feed cards */

/* Batch 37 Rev 4 — Sticky feed header + tab bar */
.profile-feed-header-sticky {
  position: sticky; top: 0; z-index: 5;
  background: var(--ink);
  /* Batch 228 R1 — sliver fix: was padding-bottom:0.6rem + margin-bottom:0.75rem.
     The transparent margin let the post image scrolling under the sticky bar bleed
     through the seam. Folded the margin into padding (0.6+0.75=1.35rem) so the
     opaque --ink background covers the full band down to the content. Same total
     spacing, no transparent gap. */
  padding-bottom: 1.35rem;
  margin-bottom: 0;
}
.profile-feed-tabs {
  display: flex; width: 100%;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  overflow: hidden;
}
.profile-feed-tab {
  flex: 1; padding: 0.5rem 0;
  font-family: 'Cinzel', serif; font-size: 0.72rem;
  letter-spacing: 0.06em; color: var(--text-muted);
  background: var(--ink); border: none; cursor: pointer;
  border-right: 1px solid var(--border);
  transition: background 0.15s, color 0.15s;
}
.profile-feed-tab:last-child { border-right: none; }
.profile-feed-tab:hover { color: var(--parchment); background: var(--ash-mid); }
.profile-feed-tab.active {
  color: var(--sepia); background: var(--ash-mid);
  font-weight: 700;
}

/* Scoring system placeholder */

/* About / Bio edit */
/* Followed forums list */
.followed-forum-item {
  display: flex; align-items: center; gap: 0.75rem;
  padding: 0.65rem; border-radius: var(--radius);
  border: 1px solid var(--border); margin-bottom: 0.5rem;
  cursor: pointer; transition: all 0.2s;
}
.followed-forum-item:hover { border-color: var(--border-strong); background: rgba(201,169,110,0.04); }

/* Reason for joining */

/* Recent activity */
.activity-item { padding: 0.6rem 0; border-bottom: 1px solid var(--border); font-size: 0.92rem; color: var(--parchment-dark); }
.activity-item:last-child { border-bottom: none; }
.activity-item span { color: var(--sepia); }

/* ============================================================
   COUNCIL BUILD — NEW STYLES
   ============================================================ */

/* ARCHITECTURE — Thaddeus, Roxanne, Shamu retired. Kemi remains as agent with Edge Function (kemi-edge-function/index.ts). Egg and Sovereign are invisible shadow monitors in egg.js and sovereign.js. Session persistence via Supabase Auth JWT. get-ip-edge-function/index.ts serves Egg. */

/* Session Warning Overlay — triggers at 60 min inactivity, lock at 120 min */
.session-warning-overlay {
  position: fixed;
  top: 0; left: 0; width: 100%; height: 100%;
  background: rgba(13,11,9,0.85);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 9999;
  backdrop-filter: blur(4px);
}
.session-warning-overlay.hidden { display: none; }
.session-warning-box {
  background: var(--ash);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-lg);
  padding: 2.5rem;
  max-width: 420px;
  width: 90%;
  text-align: center;
}
.session-warning-icon {
  font-size: 2.5rem;
  margin-bottom: 1rem;
}
.session-warning-title {
  font-family: Cinzel, serif;
  font-size: 1.1rem;
  color: var(--parchment);
  margin-bottom: 0.75rem;
  letter-spacing: 0.08em;
}
.session-warning-body {
  color: var(--parchment-dark);
  font-size: 0.95rem;
  line-height: 1.6;
  margin-bottom: 1.5rem;
}
.session-warning-body strong {
  color: var(--sepia);
  font-size: 1.2rem;
}

/* Profile Picture */
.user-avatar img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  border-radius: 50%;
}

/* Sovereign Panel */
#sovereignPanel {
  border-top: 2px solid var(--sepia);
  margin-top: 1.5rem;
  padding-top: 1.5rem;
}
#sovereignPanel.hidden { display: none; }

/* Admin user row enhancements */

/* Batch 50 — kemiStrikeBody/shamuStrikeBody/violationsReport CSS removed (orphaned modals removed) */

/* ============================================================
   SUPABASE BUILD — SITE LOADER
   ============================================================ */

.site-loader {
  position: fixed;
  top: 0; left: 0; width: 100%; height: 100%;
  background: var(--ink);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 99998;
}
.site-loader.hidden { display: none; }
.site-loader-inner {
  text-align: center;
}
.site-loader-icon {
  font-size: 2.5rem;
  color: var(--sepia);
  margin-bottom: 1rem;
  animation: pulse 1.5s ease-in-out infinite;
}
.site-loader-text {
  font-family: Cinzel, serif;
  font-size: 0.85rem;
  letter-spacing: 0.15em;
  color: var(--text-muted);
  text-transform: uppercase;
}
@keyframes pulse {
  0%, 100% { opacity: 1; }
  50%       { opacity: 0.3; }
}

/* ============================================================
   USER LIBRARY — COLOR CODED STATUS ROWS
   ============================================================ */

.user-lib-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0.6rem 0.9rem;
  border-radius: var(--radius);
  margin-bottom: 0.4rem;
  cursor: pointer;
  transition: opacity 0.15s;
  border: 1px solid transparent;
}
.user-lib-row:hover { opacity: 0.85; }

.user-lib-row.status-red    { background: rgba(160,30,30,0.18); border-color: rgba(160,30,30,0.4); }
.user-lib-row.status-green  { background: rgba(30,100,40,0.12); border-color: rgba(30,100,40,0.3); }

.user-lib-name {
  font-family: Cinzel, serif;
  font-size: 0.82rem;
  letter-spacing: 0.06em;
}
.user-lib-name.status-red    { color: #e06060; }
.user-lib-name.status-orange { color: #e09040; }
.user-lib-name.status-yellow { color: #c8b830; }
.user-lib-name.status-green  { color: #60b870; }

.user-lib-meta {
  font-size: 0.78rem;
  color: var(--text-muted);
}

.user-lib-badge {
  font-size: 0.72rem;
  font-family: Cinzel, serif;
  letter-spacing: 0.08em;
  padding: 0.15rem 0.5rem;
  border-radius: 2rem;
  margin-left: 0.5rem;
}
.user-lib-badge.red    { background: rgba(160,30,30,0.35); color: #e06060; }
.user-lib-badge.orange { background: rgba(180,90,20,0.35); color: #e09040; }
.user-lib-badge.yellow { background: rgba(160,140,20,0.35); color: #c8b830; }
.user-lib-badge.green  { background: rgba(30,100,40,0.25); color: #60b870; }
/* Batch 195 R2 — report badge drops to its own line below the strike badge and
   reads as a single pill ("1 Report" / "2 Reports"), instead of crowding the
   strike badge inline (which made the label wrap awkwardly). Block + no left
   margin so the pill aligns under the name; small top gap separates the rows. */
.user-lib-badge.report-line {
  display: inline-block;
  margin-left: 0;
  margin-top: 0.3rem;
}

/* ============================================================
   USER LIBRARY POPUP
   ============================================================ */

.user-popup-header {
  display: flex;
  align-items: center;
  gap: 1rem;
  margin-bottom: 1.5rem;
  padding-bottom: 1rem;
  border-bottom: 1px solid var(--border);
}
.user-popup-username {
  font-family: Cinzel, serif;
  font-size: 1.1rem;
  letter-spacing: 0.1em;
}
.user-popup-section {
  margin-bottom: 1.5rem;
}
.user-popup-section-title {
  font-family: Cinzel, serif;
  font-size: 0.75rem;
  letter-spacing: 0.15em;
  color: var(--sepia);
  margin-bottom: 0.75rem;
  padding-bottom: 0.35rem;
  border-bottom: 1px solid var(--border);
}
.violation-tag {
  font-size: 0.7rem;
  font-family: Cinzel, serif;
  letter-spacing: 0.06em;
  padding: 0.1rem 0.4rem;
  border-radius: 0.2rem;
  flex-shrink: 0;
}
.violation-tag.iv { background: rgba(160,30,30,0.3); color: #e06060; }
.violation-tag.kv { background: rgba(30,80,160,0.3); color: #6090e0; }

/* ============================================================
   VIOLATIONS SECTION
   ============================================================ */

.violation-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0.5rem 0;
  border-bottom: 1px solid var(--border);
  font-size: 0.85rem;
}
.violation-date {
  font-size: 0.75rem;
  color: var(--text-muted);
}

/* ============================================================
   TIER BADGES
   ============================================================ */

.tier-badge {
  display: inline-block;
  font-family: Cinzel, serif;
  font-size: 0.65rem;
  font-weight: 700;
  letter-spacing: 0.05em;
  padding: 0.1rem 0.35rem;
  border-radius: 0.2rem;
  margin-left: 0.3rem;
  vertical-align: middle;
  position: relative;
  top: -1px;
}

/* Batch 50 — .merchant-pending-btn removed (merchant subsystem wiped Batch 44, zero references) */

/* ============================================================
   SHOP FILTERS
   ============================================================ */

.shop-filter-label input { cursor: pointer; accent-color: var(--sepia); }

/* ============================================================
   METRA-EXPANSE
   ============================================================ */


/* ============================================================
   GALLERY DISCOVERY FEED
   ============================================================ */

.gallery-feed-card {
  background: var(--ash-mid);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  padding: 1rem;
  margin-bottom: 1.25rem;
  max-width: 620px;
  position: relative;
}
.gallery-feed-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 0.75rem;
  position: relative;
}

/* ============================================================
   THREE-DOT POST MENU
   ============================================================ */

.post-menu-btn {
  cursor: pointer;
  color: var(--text-muted);
  font-size: 1.2rem;
  padding: 0 0.25rem;
  line-height: 1;
  user-select: none;
  margin-left: auto;
  /* Batch 174 — Card-expansion fix. Establish the kebab button as the positioning
     context for its sibling .post-menu (which is position:absolute). Previously the
     menu anchored to the nearest positioned ancestor — .post-card — and with .post-card
     no longer clipping (overflow:hidden removed in a prior pass), the tall menu painted
     past the short card's lower border, reading as the card "expanding." Anchoring to
     the button makes the menu overlay as a true floating dropdown. Shared by all 20
     .post-menu render sites via this class — retroactive. The opened-post path already
     wraps btn+menu in its own absolute container, so re-anchoring is consistent there. */
  position: relative;
}
.post-menu-btn:hover { color: var(--parchment); }
.post-menu {
  position: absolute;
  top: 1.5rem;
  right: 0;
  background: #1a1814;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  min-width: 160px;
  z-index: 9999;
  box-shadow: 0 8px 32px rgba(0,0,0,0.85);
  opacity: 1 !important;
  isolation: isolate;
}
.post-menu div {
  padding: 0.5rem 0.85rem;
  font-size: 0.85rem;
  color: var(--parchment-dark);
  cursor: pointer;
  transition: background 0.12s;
}
.post-menu div:hover { background: #2a2620; color: var(--parchment); }

/* ============================================================
   NOTIFICATIONS
   ============================================================ */


/* Batch 50 — Merchant Profile CSS removed (merchant subsystem wiped Batch 44, zero references) */

/* ============================================================
   PROFILE RECENT POSTS / COMMENTS
   ============================================================ */


.profile-card-wide {
  grid-column: 1 / -1;
}

/* ============================================================
   POST AUTHOR AVATAR (Rev 4)
   ============================================================ */
.post-author-avatar {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 28px;
  height: 28px;
  border-radius: 50%;
  background: linear-gradient(135deg, var(--sepia-dark), var(--sepia));
  color: var(--ink);
  font-weight: 700;
  font-family: 'Cinzel', serif;
  font-size: 0.7rem;
  border: 1px solid var(--border-strong);
  cursor: pointer;
  flex-shrink: 0;
  overflow: hidden;
  vertical-align: middle;
  margin-right: 0.4rem;
  transition: border-color 0.15s;
}
.post-author-avatar:hover { border-color: var(--sepia); }
.post-author-avatar img { width: 100%; height: 100%; object-fit: cover; border-radius: 50%; }

/* ============================================================
   DM FLOATING PANEL (Rev 3)
   ============================================================ */
.dm-thread-row {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  padding: 0.65rem 1rem;
  border-bottom: 1px solid var(--border);
  cursor: pointer;
  transition: background 0.12s;
}
.dm-thread-row:hover { background: var(--ash-mid); }
.dm-thread-row.dm-unread { background: rgba(201,169,110,0.06); }
.dm-thread-avatar {
  width: 36px;
  height: 36px;
  border-radius: 50%;
  background: linear-gradient(135deg, var(--sepia-dark), var(--sepia));
  color: var(--ink);
  font-weight: 700;
  font-family: 'Cinzel', serif;
  font-size: 0.8rem;
  display: flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  overflow: hidden;
  border: 1px solid var(--border-strong);
}
.dm-thread-avatar img { width: 100%; height: 100%; object-fit: cover; border-radius: 50%; }
.dm-thread-info { flex: 1; min-width: 0; }
.dm-thread-name {
  font-size: 0.85rem;
  color: var(--parchment);
  font-weight: 600;
  display: flex;
  align-items: center;
  gap: 0.4rem;
}
.dm-thread-preview {
  font-size: 0.75rem;
  color: var(--text-muted);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.dm-thread-time {
  font-size: 0.7rem;
  color: var(--text-muted);
  flex-shrink: 0;
}
.dm-thread-badge {
  background: #e06060;
  color: #fff;
  font-size: 0.62rem;
  font-family: 'Cinzel', serif;
  border-radius: 50%;
  width: 16px;
  height: 16px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-weight: 700;
}

/* ============================================================
   REVISION LOG — BATCH 3 (FG Opus Claude)
   ============================================================
   Rev 1-6: No changes to styles.css.
   ============================================================ */


/* ============================================================
   REVISION LOG — BATCH 4 (FG Opus Claude)
   ============================================================
   Issues 1-5: No changes to styles.css.
   ============================================================ */


/* ============================================================
   REVISION LOG — BATCH 5 (FG Opus Claude)
   ============================================================
   Rev 1-4: No changes to styles.css.
   ============================================================ */


/* ============================================================
   REVISION LOG — BATCH 6 (FG Opus Claude)
   ============================================================
   Avatar Token System: No changes to styles.css.
   ============================================================ */

/* ============================================================
   REVISION LOG — BATCH 7 (FG Opus Claude)
   ============================================================
   Rev 1 — Reply system + Notification bell:
     Reply thread styling, comment action buttons, notification
     floating panel categories.
   Rev 2 — No changes to styles.css.
   Rev 3 — Comment thread modal header, metra review section.
   Rev 4 — Hover transforms removed from .post-card and .forum-card.
     .post-card-image changed to object-fit:contain, no fixed height.
   Rev 5 — .post-card overflow:hidden removed, position:relative added.
     .comment-body white-space:pre-wrap added. .forum-formatted-text,
     .forum-textarea styles added for paragraph formatting.
   Rev 6 — .forum-card-lore gold styling for lore category posts.
   ============================================================ */

/* ---- REPLY SYSTEM ---- */
.comment-replies-wrapper {
  margin-left: 1.5rem;
  border-left: 2px solid var(--border);
  padding-left: 0.75rem;
  margin-top: 0.5rem;
}
.comment-reply {
  padding: 0.5rem 0.5rem;
  margin-bottom: 0.4rem;
  border-bottom: 1px solid rgba(255,255,255,0.04);
}
.comment-reply:last-child { border-bottom: none; }
/* Batch 11 — Neon purple @tag highlights */
.tag-mention {
  color: #bf5fff;
  font-weight: 600;
  font-size: 0.85rem;
  cursor: pointer;
  text-shadow: 0 0 6px rgba(191,95,255,0.3);
}
.tag-mention:hover { text-decoration: underline; color: #d580ff; }
.comment-actions {
  display: flex;
  gap: 0.75rem;
  margin-top: 0.35rem;
  align-items: center;
}
.comment-action-btn {
  background: none;
  border: none;
  color: var(--text-muted);
  font-size: 0.78rem;
  cursor: pointer;
  padding: 0;
  font-family: 'Crimson Pro', serif;
  transition: color 0.15s;
}
.comment-action-btn:hover { color: var(--parchment); }
.comment-action-btn.liked { color: #e06060; }
.reply-toggle-btn {
  background: none;
  border: none;
  color: var(--sepia);
  font-size: 0.78rem;
  font-family: 'Cinzel', serif;
  letter-spacing: 0.04em;
  cursor: pointer;
  padding: 0.25rem 0;
  margin-top: 0.25rem;
}
.reply-toggle-btn:hover { text-decoration: underline; }
.reply-input-area {
  display: flex;
  gap: 0.5rem;
  margin-top: 0.5rem;
  align-items: center;
}
.reply-input-area input {
  flex: 1;
  background: var(--ash-mid);
  border: 1px solid var(--border);
  color: var(--parchment);
  padding: 0.4rem 0.65rem;
  border-radius: var(--radius);
  font-family: 'Crimson Pro', serif;
  font-size: 0.88rem;
  outline: none;
}
.reply-input-area input:focus { border-color: var(--sepia); }

/* ---- FORUM PARAGRAPH FORMATTING ---- */
.forum-formatted-text {
  white-space: pre-wrap;
  word-wrap: break-word;
  font-size: 0.95rem;
  color: var(--parchment-dark);
  line-height: 1.7;
}
.forum-textarea {
  width: 100%;
  background: var(--ash-mid);
  border: 1px solid var(--border);
  color: var(--parchment);
  padding: 0.5rem 0.65rem;
  border-radius: var(--radius);
  font-family: 'Crimson Pro', serif;
  font-size: 0.9rem;
  line-height: 1.6;
  resize: vertical;
  outline: none;
  min-height: 60px;
  tab-size: 4;
}
.forum-textarea:focus { border-color: var(--sepia); }

/* ---- FORUM — NETFLIX-STYLE TOPIC ROWS (Batch 11) ---- */
.forum-topic-row {
  margin-bottom: 2rem;
}
.forum-row-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 0.75rem;
}
.forum-row-title {
  font-family: 'Cinzel', serif;
  font-size: 1rem;
  letter-spacing: 0.06em;
  color: var(--parchment);
}
.forum-row-arrows {
  display: flex;
  gap: 0.4rem;
}
.forum-arrow-btn {
  background: var(--ash-mid);
  border: 1px solid var(--border);
  color: var(--sepia);
  width: 32px;
  height: 32px;
  border-radius: 50%;
  cursor: pointer;
  font-size: 0.9rem;
  display: flex;
  align-items: center;
  justify-content: center;
  transition: background 0.15s, border-color 0.15s;
}
.forum-arrow-btn:hover {
  background: var(--ash-light);
  border-color: var(--sepia);
}
.forum-row-scroll {
  display: flex;
  gap: 1rem;
  overflow-x: auto;
  scroll-behavior: smooth;
  padding-bottom: 0.5rem;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: thin;
  scrollbar-color: var(--ash-light) transparent;
}
.forum-row-scroll::-webkit-scrollbar {
  height: 6px;
}
.forum-row-scroll::-webkit-scrollbar-track {
  background: transparent;
}
.forum-row-scroll::-webkit-scrollbar-thumb {
  background: var(--ash-light);
  border-radius: 3px;
}
.forum-square-card {
  flex: 0 0 220px;
  width: 220px;
  background: var(--ash-mid);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  overflow: hidden;
  cursor: pointer;
  transition: border-color 0.2s, box-shadow 0.2s;
  display: flex;
  flex-direction: column;
  position: relative;
}
.forum-square-card:hover {
  border-color: var(--sepia);
  box-shadow: 0 4px 20px rgba(0,0,0,0.3);
}
.forum-square-card-lore {
  border-color: rgba(180,140,20,0.4);
  background: rgba(180,140,20,0.08);
}
.forum-card-header {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.6rem 0.7rem 0.4rem;
}
.forum-card-header .post-author-avatar {
  width: 28px;
  height: 28px;
  font-size: 0.55rem;
}
.forum-card-username {
  font-size: 0.78rem;
  font-weight: 600;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  max-width: 130px;
}
.forum-card-bold-title {
  font-family: 'Cinzel', serif;
  font-size: 0.82rem;
  font-weight: 700;
  color: var(--parchment);
  padding: 0 0.7rem 0.4rem;
  line-height: 1.3;
}
.forum-card-image {
  width: 100%;
  aspect-ratio: 1 / 1;
  object-fit: cover;
  display: block;
}
.forum-card-actions {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  padding: 0.5rem 0.7rem;
  border-top: 1px solid var(--border);
}
.forum-card-actions .comment-action-btn {
  font-size: 0.75rem;
}
.forum-card-actions span {
  font-size: 0.75rem;
  color: var(--text-muted);
}
.forum-card-time {
  font-size: 0.68rem;
  color: var(--text-muted);
  padding: 0 0.7rem 0.1rem;
}
/* Topic checkbox styling */
.forum-topic-check {
  display: flex;
  align-items: center;
  gap: 0.3rem;
  font-size: 0.82rem;
  color: var(--parchment-dark);
  cursor: pointer;
  padding: 0.3rem 0.6rem;
  background: var(--ash-mid);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  transition: border-color 0.15s;
}
.forum-topic-check:has(input:checked) {
  border-color: var(--sepia);
  background: rgba(180,140,20,0.1);
}
.forum-topic-check input {
  accent-color: var(--sepia);
}
/* Empty row state */
.forum-row-empty {
  color: var(--text-muted);
  font-size: 0.85rem;
  font-style: italic;
  padding: 1.5rem 0;
}

/* ---- NOTIFICATION FLOATING PANEL ---- */
.notif-item {
  display: flex;
  align-items: flex-start;
  gap: 0.6rem;
  padding: 0.5rem 0.35rem;
  border-bottom: 1px solid var(--border);
  cursor: pointer;
  transition: background 0.15s;
  border-radius: var(--radius);
}
.notif-item:hover { background: rgba(180,140,20,0.06); }
.notif-item.unread { background: rgba(180,140,20,0.12); box-shadow: inset 0 0 0 1px rgba(212,160,23,0.35), 0 0 8px rgba(212,160,23,0.15); }
.notif-item-icon {
  font-size: 1rem;
  flex-shrink: 0;
  width: 24px;
  text-align: center;
  margin-top: 0.1rem;
}
.notif-item-body { flex: 1; min-width: 0; }
.notif-item-text {
  font-size: 0.82rem;
  color: var(--parchment-dark);
  line-height: 1.4;
}
.notif-item-text strong { color: var(--parchment); }
.notif-item-time {
  font-size: 0.7rem;
  color: var(--text-muted);
  margin-top: 0.15rem;
}


/* ============================================================
   BATCH 15 Rev 5 — Tag Autocomplete Dropdown
   ============================================================
   .tag-autocomplete and .tag-autocomplete-item added for the
   Instagram-style @tag dropdown filtered to followers only.
   ============================================================ */
.tag-autocomplete {
  position: absolute;
  z-index: 9999;
  background: var(--ash);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius);
  box-shadow: 0 4px 20px rgba(0,0,0,0.5);
  max-height: 200px;
  overflow-y: auto;
  width: 240px;
  display: none;
}
.tag-autocomplete-item {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.45rem 0.65rem;
  cursor: pointer;
  transition: background 0.12s;
}
.tag-autocomplete-item:hover {
  background: rgba(180,140,20,0.1);
}
.shop-tab {
  flex: none;
  padding: 0.55rem 1rem;
  font-family: Cinzel, serif;
  font-size: 0.7rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  background: none;
  border: none;
  cursor: pointer;
  color: var(--text-muted);
  border-bottom: 2px solid transparent;
  transition: color 0.15s, border-color 0.15s;
}
.shop-tab:hover { color: var(--parchment); }
.shop-tab.active {
  color: var(--sepia);
  border-bottom-color: var(--sepia);
}

/* ---- ADMIN PANEL TABS (Batch 22) ---- */
.admin-tab {
  flex: 1;
  padding: 0.55rem 1rem;
  font-family: Cinzel, serif;
  font-size: 0.72rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  background: none;
  border: none;
  cursor: pointer;
  color: var(--text-muted);
  border-bottom: 2px solid transparent;
  transition: color 0.15s, border-color 0.15s;
}
.admin-tab:hover { color: var(--parchment); }
.admin-tab.active {
  color: var(--sepia);
  border-bottom-color: var(--sepia);
}
.admin-metric-card {
  background: var(--ash-mid);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 1rem;
  text-align: center;
  cursor: pointer;
  transition: border-color 0.2s, box-shadow 0.2s;
}
.admin-metric-card:hover {
  border-color: var(--sepia);
  box-shadow: 0 2px 12px rgba(0,0,0,0.3);
}
.admin-metric-num {
  font-family: Cinzel, serif;
  font-size: 1.8rem;
  color: var(--sepia);
  display: block;
  margin-bottom: 0.15rem;
}
.admin-metric-label {
  font-size: 0.78rem;
  color: var(--text-muted);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  font-family: Cinzel, serif;
}
.admin-metric-delta {
  font-size: 0.72rem;
  margin-top: 0.2rem;
}
.admin-scrollable-window {
  max-height: 220px;
  overflow-y: auto;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 0.5rem;
  background: var(--ash);
}

/* ============================================================
   BATCH 29 — Violation section styling (Critique 4)
   ============================================================ */
.violation-section {
  border-left: 3px solid #c8b830;
  padding-left: 0.6rem;
}
.violation-subblock {
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 0.75rem;
  margin-bottom: 0.6rem;
  background: rgba(160,140,20,0.06);
  display: flex;
  flex-direction: column;
  gap: 0.6rem;
  transition: background 0.15s;
}
.violation-subblock:hover {
  background: rgba(160, 140, 20, 0.10);
}

/* ============================================================
   BATCH 29 revision log
   ============================================================
   Critique 4 — Added .violation-section (left gold border marker)
     and .violation-subblock (per-flag group container) styling.
     Existing .user-lib-badge.yellow reused for (N Alerts) badge
     in User Library and Most Wanted rows.
   Critiques 1, 2, 3, 5, 6: No CSS changes.
   ============================================================ */

/* ============================================================
   BATCH 30 — Verdict system styling (Critique 2)
   ============================================================ */

/* ACCESS CODE word tokens — green when active, gray when struck.
   Per Autarch's directive: gray = dead, no line-through. */
.access-code-row {
  font-family: 'Crimson Pro', serif;
  font-size: 1rem;
  padding: 0.5rem 0.75rem;
  background: var(--ash-mid);
  border-radius: var(--radius);
  border: 1px solid var(--border);
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.1rem;
}
.access-code-word {
  color: #60b870;
  font-weight: 500;
  transition: color 0.15s;
}
.access-code-word.dead {
  color: #6a6a60;
  font-weight: 400;
  opacity: 0.75;
}
.access-code-sep {
  color: var(--text-muted);
  opacity: 0.6;
}

/* Verdict section — slight visual separation from violation above */
.verdict-section {
  border-left: 3px solid var(--sepia);
  padding-left: 0.6rem;
}

/* Reduce Strike button visual hint when user has 0 strikes */
.btn-ghost[data-no-strikes="true"] {
  opacity: 0.65;
}

/* ============================================================
   BATCH 30 revision log
   ============================================================
   Critique 2 — Added:
     - .access-code-row (container), .access-code-word (green active),
       .access-code-word.dead (grayed, no line-through per directive),
       .access-code-sep (middot separator)
     - .violation-postcard-thumb (64px post image in violation block — superseded in Batch 31)
     - .verdict-section (left sepia border to mirror violation-section styling)
     - .btn-ghost[data-no-strikes] opacity hint for Reduce Strike when 0 strikes
   Critiques 1, 3, 4, 5, 6, 7: No CSS changes.
   ============================================================ */

/* ============================================================
   BATCH 31 — Security protocol overhaul styles
   ============================================================ */

/* ---- User popup tab bar (Dossier / User Correspondence) ---- */
.user-popup-tab-bar {
  display: flex;
  gap: 0.25rem;
  border-bottom: 1px solid var(--border);
  margin-bottom: 1rem;
}
.user-popup-tab {
  background: transparent;
  border: none;
  padding: 0.6rem 1rem;
  color: var(--text-muted);
  font-family: Cinzel, serif;
  font-size: 0.78rem;
  letter-spacing: 0.08em;
  cursor: pointer;
  border-bottom: 2px solid transparent;
  transition: color 0.15s, border-color 0.15s;
}
.user-popup-tab:hover {
  color: var(--parchment);
}
.user-popup-tab.active {
  color: var(--sepia);
  border-bottom-color: var(--sepia);
}

/* ---- Severity badges on violation subblocks ---- */
.violation-severity {
  display: inline-block;
  font-family: Cinzel, serif;
  font-size: 0.65rem;
  letter-spacing: 0.12em;
  font-weight: 700;
  padding: 0.2rem 0.55rem;
  border-radius: 3px;
  margin-bottom: 0.3rem;
}
.violation-severity.death-row {
  background: rgba(160,30,30,0.35);
  color: #e06060;
  border: 1px solid rgba(160,30,30,0.6);
  text-transform: uppercase;
}
.violation-severity.three-strike {
  background: rgba(180,90,20,0.35);
  color: #e09040;
  border: 1px solid rgba(180,90,20,0.6);
  text-transform: uppercase;
}

/* ---- Violation subblock severity borders ---- */
.violation-subblock.severity-death-row {
  border-left: 4px solid #e06060 !important;
  background: rgba(160,30,30,0.08) !important;
}
.violation-subblock.severity-three-strike {
  border-left: 4px solid #e09040 !important;
  background: rgba(180,90,20,0.06) !important;
}

/* ---- Violation subblock child layout (info / preview / actions) ---- */
.violation-info {
  display: flex;
  flex-direction: column;
  gap: 0.3rem;
}
.violation-code-line {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  flex-wrap: wrap;
}
.violation-code-label {
  font-family: Cinzel, serif;
  font-size: 0.72rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-muted);
}
.violation-code-caption {
  font-size: 0.78rem;
  color: var(--parchment-dark);
  font-style: italic;
}
.violation-context {
  font-size: 0.87rem;
  color: var(--parchment);
  line-height: 1.45;
}
.violation-tracking-id {
  font-family: 'Crimson Pro', serif;
  font-size: 0.7rem;
  color: var(--text-muted);
  letter-spacing: 0.02em;
  word-break: break-all;
}

/* Preview box — sits between info and action row per Autarch directive.
   Variants: .image (thumbnail), .keyword (excerpt card), .placeholder
   (dashed empty-state for historical/no-data records). */
.violation-preview-box {
  background: var(--ash-mid);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 0.55rem 0.7rem;
  display: flex;
  flex-direction: column;
  gap: 0.3rem;
  max-width: 100%;
}
.violation-preview-box.image {
  padding: 0;
  overflow: hidden;
  max-width: 280px;
  background: var(--ink);
}
.violation-preview-box.image img {
  width: 100%;
  max-height: 200px;
  object-fit: cover;
  display: block;
  border-radius: var(--radius);
}
.violation-preview-box.keyword {
  max-width: 420px;
}
.violation-preview-box.placeholder {
  border-style: dashed;
  background: rgba(201,169,110,0.04);
  max-width: 420px;
}
.violation-preview-label {
  font-family: Cinzel, serif;
  font-size: 0.62rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--text-muted);
}
.violation-preview-excerpt {
  font-family: 'Crimson Pro', serif;
  font-size: 0.85rem;
  color: var(--parchment);
  font-style: italic;
  line-height: 1.5;
  word-break: break-word;
}

/* Action row — Guilty / Close Case / View Full Post — sits at bottom,
   separated from the preview by a subtle rule. */
.violation-action-row {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem;
  padding-top: 0.45rem;
  border-top: 1px solid var(--border);
}

/* Batch 92 Fix 6 — .violation-tag.ivai removed (merchant protocol removed Batch 70; unreachable). */

/* ---- Weight indicator on per-alert buttons (e.g., IV 3x STRIKE) ---- */
.violation-weight-badge {
  display: inline-block;
  margin-left: 0.4rem;
  background: rgba(160,30,30,0.25);
  color: #e06060;
  font-size: 0.6rem;
  font-family: Cinzel, serif;
  letter-spacing: 0.1em;
  padding: 0.1rem 0.35rem;
  border-radius: 3px;
  font-weight: 700;
}

/* ---- Per-alert verdict staging (Guilty / Close Case) ---- */
.verdict-flag-staged {
  box-shadow: 0 0 0 2px var(--sepia) inset;
}

/* Issue Strike counter pulse when staged strikes > 0 */
.verdict-strike-active {
  box-shadow: 0 0 12px rgba(201,169,110,0.4);
}

/* Per-user verdict selection highlight (Batch 31 Phase 3). Applied to
   whichever of Issue Strike / Reduce / Close Case / Clear Strikes the
   admin has clicked. Distinct from .verdict-flag-staged (per-alert). */
.verdict-selected {
  box-shadow: 0 0 0 2px var(--sepia) inset, 0 0 14px rgba(201,169,110,0.5);
}

/* ---- Ban User dropdown ---- */
.verdict-ban-wrapper {
  position: relative;
  display: inline-block;
}
.verdict-ban-dropdown {
  position: absolute;
  top: 100%;
  left: 0;
  margin-top: 0.3rem;
  background: var(--ash);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 0.3rem;
  min-width: 180px;
  z-index: 10;
  box-shadow: 0 6px 20px rgba(0,0,0,0.4);
}
.verdict-ban-dropdown.hidden {
  display: none;
}
.verdict-ban-option {
  padding: 0.5rem 0.7rem;
  cursor: pointer;
  color: var(--parchment);
  font-size: 0.85rem;
  border-radius: var(--radius);
  transition: background 0.12s;
}
.verdict-ban-option:hover {
  background: rgba(160,30,30,0.25);
  color: #e06060;
}
.verdict-ban-option.verdict-ban-clear {
  color: var(--text-muted);
  border-top: 1px solid var(--border);
  margin-top: 0.2rem;
  font-style: italic;
}
.verdict-ban-option.verdict-ban-clear:hover {
  background: rgba(201,169,110,0.1);
  color: var(--parchment);
}
.verdict-ban-staged {
  box-shadow: 0 0 0 2px #e06060 inset;
}

/* ---- Gavel commit button (inline SVG, head rests up, slams on click) ---- */
.verdict-gavel-btn {
  background: linear-gradient(135deg, var(--sepia-dark), var(--sepia));
  border: 2px solid var(--sepia);
  border-radius: 50%;
  width: 68px;
  height: 68px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  padding: 0;
  transition: box-shadow 0.2s, transform 0.15s;
  box-shadow: 0 2px 10px rgba(0,0,0,0.35);
  overflow: visible;
}
.verdict-gavel-btn:hover {
  box-shadow: 0 0 0 3px rgba(201,169,110,0.25), 0 4px 14px rgba(201,169,110,0.45);
  transform: translateY(-1px);
}
.verdict-gavel-btn:active {
  transform: translateY(0);
}
.verdict-gavel-svg {
  width: 52px;
  height: 52px;
  display: block;
  overflow: visible;
}
/* Gavel head+handle group. Pivot at (32, 52) in SVG user-space — the
   handle base center — so the head rotates around that point like a
   gavel being swung. transform-box defaults to view-box for SVG so we
   don't set it explicitly (setting fill-box would offset the origin
   relative to the group's own bounding box, breaking the pivot). */
.verdict-gavel-g {
  transform-origin: 32px 52px;
  transform: rotate(0deg);
}
/* Strike animation — wind up, slam down, small recoil, return to rest.
   Applied via .striking class added by _verdictGavel on successful commit. */
.verdict-gavel-btn.striking .verdict-gavel-g {
  animation: gavelStrike 0.6s cubic-bezier(0.55, 0.08, 0.22, 1.25);
}
@keyframes gavelStrike {
  0%   { transform: rotate(0deg); }
  25%  { transform: rotate(-55deg); }    /* wind up, head lifts back */
  50%  { transform: rotate(25deg); }     /* slam down onto block */
  65%  { transform: rotate(12deg); }     /* small recoil bounce */
  100% { transform: rotate(0deg); }      /* return to rest */
}
/* Sound block bounces when the strike lands (mirrors the gavel timing) */
.verdict-gavel-btn.striking .verdict-gavel-svg ellipse {
  animation: blockImpact 0.6s cubic-bezier(0.55, 0.08, 0.22, 1.25);
}
@keyframes blockImpact {
  0%, 45%, 100% { transform: translateY(0); }
  50%           { transform: translateY(2px); }
  60%           { transform: translateY(0); }
}

/* ---- User Correspondence inbox rows (Batch 31 Phase 3) ----
   Gmail-style list: sender left, bold subject + preview center,
   timestamp right. Replaces the chat-bubble layout with source tag.
   The prior .corr-source-tag / .dm / .email rules were removed when
   the source tag disappeared (correspondence is now email-only). */
.corr-row {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  padding: 0.6rem 0.75rem;
  border-bottom: 1px solid var(--border);
  cursor: pointer;
  transition: background 0.12s;
}
.corr-row:hover {
  background: rgba(201,169,110,0.06);
}
.corr-body {
  padding: 0.75rem 0.9rem;
  background: rgba(201,169,110,0.04);
  border-bottom: 1px solid var(--border);
  font-size: 0.88rem;
  line-height: 1.55;
  color: var(--parchment);
  white-space: pre-wrap;
}

/* ---- Death Row badge in Sovereign Reports ---- */
.death-row-badge {
  font-family: Cinzel, serif;
  font-size: 0.62rem;
  letter-spacing: 0.1em;
  padding: 0.15rem 0.45rem;
  background: rgba(160,30,30,0.4) !important;
  color: #ff7070 !important;
  border: 1px solid rgba(160,30,30,0.7);
}

/* ============================================================
   REVISION LOG — BATCH 31 (FG Opus Claude)
   ============================================================
   Security protocol overhaul styling. Final state after Phase 2
   corrections:

   Admin popup layout:
     - .user-popup-tab-bar + .user-popup-tab: tab bar for the new
       Dossier / User Correspondence layout.

   Violation subblock (vertical: info -> preview -> actions):
     - .violation-subblock: gained border/padding/column flex
       (previously inlined by JS; now class-owned).
     - .violation-info: top info block.
     - .violation-code-line + .violation-code-label +
       .violation-code-caption: inline "Violation Code: <TAG>
       <caption>" format per Autarch directive.
     - .violation-context: context sentence line.
     - .violation-tracking-id: KF-xxx tracking id as small metadata.
     - .violation-preview-box (+ .image / .keyword / .placeholder):
       preview card between info and actions. Image variant shows
       thumbnail; keyword variant shows excerpt card; placeholder
       variant (dashed border) handles historical / no-data records.
     - .violation-preview-label + .violation-preview-excerpt: label
       and content inside preview cards.
     - .violation-action-row: Guilty / Close Case / View Full Post
       bottom row with subtle top rule.
     - .violation-tag.ivai: IVAI merchant-AI category tag, completing
       the IV (red) / KV (blue) / IVAI (orange) trio.

   Severity accents:
     - .violation-severity + death-row / three-strike variants.
     - .violation-subblock.severity-death-row / severity-three-strike:
       left-border accent + tinted background.

   Verdict controls:
     - .violation-weight-badge: "3x STRIKE" marker on Guilty button
       for IVAI weight-3 alerts.
     - .verdict-flag-staged + .verdict-strike-active: staging
       indicators (per-alert stage + cumulative Nx counter).
     - .verdict-ban-wrapper + .verdict-ban-dropdown + .verdict-ban-option
       + .verdict-ban-clear + .verdict-ban-staged: Ban User dropdown.

   Gavel (inline SVG, rest state head-up, slam animation on click):
     - .verdict-gavel-btn: circular parchment-gold button (68px),
       no hover lift (gavel is already up), subtle glow on hover.
     - .verdict-gavel-svg + .verdict-gavel-g: SVG container + group
       whose transform-origin sits at the handle base so the head
       swings around it.
     - @keyframes gavelStrike: wind up (-55deg) -> slam down (+25deg)
       -> small recoil (+12deg) -> rest (0deg). 0.6s total.
     - @keyframes blockImpact: sound-block bounce on strike landing.
     - .striking class: added by _verdictGavel after validation
       passes; removed 600ms later so subsequent commits animate
       fresh.

   User Correspondence (Gmail-style list — Batch 31 Phase 3):
     - .corr-row: list row (sender left, bold subject + preview
       middle, timestamp right). :hover highlights the row.
     - .corr-body: expanded full-message body shown when a row
       is clicked via _toggleCorrMessage.

   Sovereign Reports:
     - .death-row-badge: DEATH-ROW badge in the Death Row queue.

   Removed in Phase 2 cleanup:
     - .violation-postcard-thumb (superseded by .violation-preview-box.image)
     - .violation-keyword-preview / .violation-keyword-label /
       .violation-keyword-excerpt (superseded by .violation-preview-box.keyword
       + .violation-preview-label + .violation-preview-excerpt)
     - .verdict-gavel-icon (superseded by inline SVG markup)

   Removed in Phase 3 cleanup:
     - .corr-source-tag / .corr-source-tag.dm / .corr-source-tag.email
       (source tag eliminated when correspondence became email-only;
       the chat-bubble layout was replaced with .corr-row + .corr-body).

   Added in Phase 3:
     - .verdict-selected: per-user verdict selection highlight (stronger
       than .verdict-flag-staged) applied to whichever verdict button the
       admin clicked. None of the four buttons is pre-selected on render.
     - .corr-row + :hover + .corr-body: correspondence inbox list layout.
   ============================================================ */

/* ============================================================
   Batch 32 Critique 3 — Forum Library activation buttons
   DEACTIVATE: transparent → crimson fill on hover.
   ACTIVATE:   transparent → forest green fill on hover.
   White text always. Cinzel uppercase to match Library tab bar.
   ============================================================ */
.fl-activate-btn {
  background: transparent;
  color: #ffffff;
  border: 1px solid var(--border);
  padding: 0.35rem 0.75rem;
  font-family: 'Cinzel', serif;
  font-size: 0.7rem;
  letter-spacing: 0.08em;
  cursor: pointer;
  border-radius: var(--radius);
  transition: background 0.15s ease, border-color 0.15s ease;
  flex-shrink: 0;
  white-space: nowrap;
}
.fl-activate-btn:hover {
  background: #2d6a3a;
  border-color: #2d6a3a;
  color: #ffffff;
}

/* ============================================================
   Batch 32 Critique 4 — Forum comment upvote / downvote buttons
   Default: muted text, transparent background.
   Active upvote: sepia (gold) fill.
   Active downvote: crimson fill.
   Hover: subtle bg tint toward the active color.
   Applied at BOTH forum panel (fcup-/fcdn-) and post modal (cup-/cdn-).
   ============================================================ */
.vote-btn {
  background: transparent;
  color: var(--text-muted);
  border: 1px solid var(--border);
  padding: 0.2rem 0.5rem;
  font-size: 0.72rem;
  font-family: inherit;
  cursor: pointer;
  border-radius: var(--radius);
  transition: background 0.15s ease, color 0.15s ease, border-color 0.15s ease;
  line-height: 1.1;
  display: inline-flex;
  align-items: center;
  gap: 0.15rem;
}
.vote-btn.vote-up:hover {
  background: rgba(201, 169, 110, 0.12);
  color: var(--sepia);
  border-color: var(--sepia);
}
.vote-btn.vote-down:hover {
  background: rgba(139, 26, 26, 0.15);
  color: #e06060;
  border-color: var(--crimson);
}
.vote-btn.vote-up.active {
  background: rgba(201, 169, 110, 0.25);
  color: var(--sepia);
  border-color: var(--sepia);
}
.vote-btn.vote-down.active {
  background: rgba(139, 26, 26, 0.3);
  color: #e06060;
  border-color: var(--crimson);
}

/* ============================================================
   Batch 32 Critique 1 — Admin Panel Fullscreen Tab Mode
   When body has .admin-fullscreen-mode, site chrome is suppressed
   and the admin modal is scaled to fill the viewport. Applied only
   on the ?admin=1 tab; the main-site tab is unaffected.

   Chrome hidden: top nav, community feed / all site sections,
   notification floating panel + detail, DM floating panel, ADP
   toggle (redundant in fullscreen), footer, new-post selector,
   loader, profile page overlay, and the admin modal overlay
   backdrop (since there's no site behind it to dim).
   ============================================================ */
body.admin-fullscreen-mode #topNav,
body.admin-fullscreen-mode nav,
body.admin-fullscreen-mode main,
body.admin-fullscreen-mode #homeSection,
body.admin-fullscreen-mode #forumSection,
body.admin-fullscreen-mode #gallerySection,
body.admin-fullscreen-mode #bookSection,
body.admin-fullscreen-mode #shopSection,
body.admin-fullscreen-mode #metraSection,
body.admin-fullscreen-mode #announcementBanner,
body.admin-fullscreen-mode footer,
body.admin-fullscreen-mode #siteLoader,
body.admin-fullscreen-mode #profilePageOverlay,
body.admin-fullscreen-mode #dmFloatingPanel,
body.admin-fullscreen-mode #notifFloatingPanel,
body.admin-fullscreen-mode #notifDetailPanel,
body.admin-fullscreen-mode #adpToggle,
body.admin-fullscreen-mode #pinnedForumToggle,
body.admin-fullscreen-mode #floatingToggleStack,
body.admin-fullscreen-mode #newPostSelector {
  display: none !important;
}

body.admin-fullscreen-mode {
  overflow: hidden;
}

/* Neutralize overlay backdrop, scale admin modal to viewport */
body.admin-fullscreen-mode #adminModal.modal-overlay {
  background: var(--ink);
  position: fixed;
  top: 0; left: 0; right: 0; bottom: 0;
  padding: 0;
  display: block;
  overflow: auto;
}
body.admin-fullscreen-mode #adminModal .modal-box {
  max-width: none !important;
  max-height: none !important;
  width: 100% !important;
  min-height: 100vh;
  margin: 0 !important;
  border-radius: 0 !important;
  border: none !important;
  padding: 1.5rem 2rem !important;
  box-shadow: none !important;
}
body.admin-fullscreen-mode #adminModal .modal-title {
  font-size: 1.5rem;
}

/* ============================================================
   Batch 169 — Collective Board (permanent left column) + two-column
   admin workspace. The Board holds New Members, Admin Job Applications,
   and the User Library. It is side-by-side with the tab content at ALL
   viewport widths (phone + desktop) and collapses horizontally to a thin
   spine for better viewability of the Security / Site Stats tabs.
   ============================================================ */
.admin-workspace {
  display: flex;
  align-items: flex-start;
  gap: 1.5rem;
  width: 100%;
}
.admin-collective-board {
  /* Clamp scales the expanded width across the viewport range. */
  flex: 0 0 clamp(280px, 30vw, 420px);
  width: clamp(280px, 30vw, 420px);
  min-width: 0;
  border-right: 1px solid var(--border);
  padding-right: 1.25rem;
  position: relative;
  transition: flex-basis 0.18s ease, width 0.18s ease, padding 0.18s ease;
}
.admin-tab-column {
  flex: 1 1 auto;
  min-width: 0;
}
/* Collapse control (chevron at the board's top-right while expanded). */
.admin-board-collapse-btn {
  position: absolute;
  top: 0; right: 0.5rem;
  background: var(--ash-mid);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  color: var(--sepia);
  font-size: 1rem;
  line-height: 1;
  padding: 0.2rem 0.5rem;
  cursor: pointer;
  z-index: 2;
}
.admin-board-collapse-btn:hover { background: var(--ash); }
/* Spine (shown only when collapsed): vertical expand handle + label. */
.admin-board-spine {
  display: none;
  width: 100%;
  background: var(--ash-mid);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  color: var(--sepia);
  cursor: pointer;
  padding: 0.75rem 0.25rem;
  flex-direction: column;
  align-items: center;
  gap: 0.75rem;
  font-family: 'Cinzel', serif;
}
.admin-board-spine:hover { background: var(--ash); }
.admin-board-spine-arrow { font-size: 1.1rem; line-height: 1; }
.admin-board-spine-label {
  writing-mode: vertical-rl;
  text-orientation: mixed;
  letter-spacing: 0.12em;
  font-size: 0.72rem;
  text-transform: uppercase;
  white-space: nowrap;
}
/* Collapsed state: board shrinks to the spine; content + collapse chevron hide. */
.admin-collective-board.collapsed {
  flex: 0 0 auto;
  width: auto;
  padding-right: 0;
  border-right: none;
}
.admin-collective-board.collapsed .admin-board-content,
.admin-collective-board.collapsed .admin-board-collapse-btn { display: none; }
.admin-collective-board.collapsed .admin-board-spine { display: flex; }

/* ============================================================
   Batch 33 Critique 1 — Pin Forum feature styles
   Pin button: outlined box with pushpin glyph. Hover fills sepia
   gold as visual affordance of what pinning does. After pin, the
   button swaps to a plain-text "Unpin" action (no box).
   Ribbon: appears above forum title when 2+ forums are pinned
   AND panel was opened from the pin toggle. Horizontal scrollable
   strip of forum-title tabs with per-forum badge counts.
   ============================================================ */
.pin-btn {
  background: transparent;
  color: var(--parchment);
  border: 1px solid var(--border-strong);
  padding: 0.35rem 0.55rem;
  font-size: 1rem;
  cursor: pointer;
  border-radius: var(--radius);
  transition: background 0.15s ease, color 0.15s ease, border-color 0.15s ease;
  line-height: 1;
  flex-shrink: 0;
}
.pin-btn:hover {
  background: var(--sepia);
  color: var(--ink);
  border-color: var(--sepia);
}
.pin-btn:active {
  background: var(--sepia-light);
  color: var(--ink);
  border-color: var(--sepia-light);
}
.unpin-btn {
  background: transparent;
  color: var(--sepia);
  border: none;
  padding: 0.35rem 0.6rem;
  font-family: 'Cinzel', serif;
  font-size: 0.72rem;
  letter-spacing: 0.08em;
  cursor: pointer;
  text-transform: uppercase;
  flex-shrink: 0;
}
.unpin-btn:hover {
  color: var(--sepia-light);
  text-decoration: underline;
}

.pinned-forum-tab {
  background: transparent;
  color: var(--text-muted);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 0.35rem 0.7rem;
  font-family: 'Cinzel', serif;
  font-size: 0.7rem;
  letter-spacing: 0.05em;
  cursor: pointer;
  white-space: nowrap;
  flex-shrink: 0;
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  transition: background 0.15s ease, color 0.15s ease, border-color 0.15s ease;
}
.pinned-forum-tab:hover {
  color: var(--parchment);
  border-color: var(--sepia-dark);
}
.pinned-forum-tab.active {
  background: var(--ash-mid);
  color: var(--sepia);
  border-color: var(--sepia);
}
.pinned-forum-tab-count {
  background: #e06060;
  color: #ffffff;
  font-family: 'Cinzel', serif;
  font-size: 0.6rem;
  padding: 0.05rem 0.4rem;
  border-radius: 10px;
  line-height: 1;
}

/* ============================================================
   REVISION LOG — BATCH 37 (FG Opus Claude)
   ============================================================
   Profile Page Restructure:

   Two-column layout:
     - .profile-columns: flex container for feed + sidebar.
     - .profile-feed-column: left column (flex:3), independently
       scrollable, max-height viewport. Custom scrollbar.
     - .profile-sidebar-column: right column (flex:2), sticky
       positioning (top:1rem), max-height viewport.
     - Mobile responsive: columns stack below 700px.

   Profile feed cards:
     - .profile-feed-card: individual card with border, padding,
       hover highlight.
     - .pf-card-meta: flex row with type badge + date.
     - .pf-card-type: Cinzel label badge (FORUM, QUOTE, etc.).
     - .pf-card-date: right-aligned relative timestamp.
     - .pf-card-title: single-line truncated title.
     - .pf-card-preview: content preview text.
     - .pf-card-action: small View button.

   Scoring placeholder:
     - .profile-scoring-note: dashed-border placeholder box
       for future scoring/credits system.

   Layout:
     - .profile-page max-width widened from 900px to 1100px.
   ============================================================ */

/* ============================================================
   REVISION LOG — BATCH 37 PHASE 3 (FG Opus Claude)
   ============================================================
   Rev 3 — Image post size standardization:
     .gallery-feed-image: aspect-ratio 1/1, object-fit cover,
     fills card width. Square max proportions. Replaces inline
     max-height:500px;object-fit:contain on gallery images.
   ============================================================ */

/* ============================================================
   REVISION LOG — BATCH 37 PHASE 4 (FG Opus Claude)
   ============================================================
   Rev 4 — Activity Feed tabbed navigation:
     .profile-feed-header-sticky: sticky header with z-index 5,
       background matches page, holds label + tab bar.
     .profile-feed-tabs: flex container, full width, bordered.
     .profile-feed-tab: equal-width buttons with Cinzel font,
       active state highlights with sepia + bold.
   ============================================================ */

/* ============================================================
   REVISION LOG — BATCH 38 (FG Opus Claude)
   ============================================================
   Rev 2 — Forum image size cap:
     .post-card-image: added max-height: 500px to match gallery
     image constraint. object-fit: contain prevents cropping.
   ============================================================ */

/* ============================================================
   REVISION LOG — BATCH 39 (FG Opus Claude)
   ============================================================
   Rev 1 — Forum feed card image structural fix:
     Added .post-card-image-wrap class (max-height:500px;
     overflow:hidden; border-radius top corners). Replaced
     previous .post-card-image rules (max-width/max-height/
     width:auto/height:auto/margin:0 auto) with width:100%;
     display:block. Image always fills card width. Wrapper
     clips overflow. HTML structure changed in app.js —
     <img> now wrapped in .post-card-image-wrap container
     in both renderPostCard() and _renderPostAsFeedCard().
   ============================================================ */

/* ============================================================
   REVISION LOG — BATCH 40 (FG Opus Claude)
   ============================================================
   Rev 1 — Forum feed card image center-crop fix:
     .post-card-image-wrap: added display:flex; align-items:center.
     Flexbox centers image vertically inside 500px-capped wrapper.
     Portrait images exceeding 500px now clip evenly from top and
     bottom instead of bottom-only. Landscape images unaffected.
     .post-card-image rule unchanged (width:100%; display:block).
   ============================================================ */

/* ============================================================
   BATCH 52 — Ranking & Badge System Styles (FG Opus Claude)
   ============================================================ */

/* Hack Alert post card — baby blue tint */
.hack-alert-card {
  border-left: 4px solid #5ba3d9 !important;
  background: rgba(91, 163, 217, 0.06) !important;
}

.hack-alert-card .post-tag {
  background: rgba(91, 163, 217, 0.2);
  color: #5ba3d9;
}

/* Council Favorite post card — gold tint */
.council-favorite-card {
  border: 2px solid #c9a84c !important;
  background: rgba(201, 168, 76, 0.15) !important;
  box-shadow: 0 0 12px rgba(201, 168, 76, 0.25), inset 0 0 20px rgba(201, 168, 76, 0.05) !important;
}

.council-favorite-card .post-tag {
  background: rgba(201, 168, 76, 0.35);
  color: #e8c14c;
  border-color: #c9a84c;
}

/* Batch 166 — Magisterium recognition: ultraviolet tint on a Magisterium
   member's deliberate universal post. Applied via .magisterium-universal-card,
   gated in renderPostCard to category 'universal' + a majesty-badge author. */
.magisterium-universal-card {
  border: 2px solid #8a4fff !important;
  background: rgba(138, 79, 255, 0.14) !important;
  box-shadow: 0 0 14px rgba(138, 79, 255, 0.35), inset 0 0 22px rgba(124, 58, 237, 0.08) !important;
}

.magisterium-universal-card .post-tag {
  background: rgba(138, 79, 255, 0.35);
  color: #c9a4ff;
  border-color: #8a4fff;
}

/* Council Favorite star button */
.council-fav-star {
  color: #c9a84c !important;
  font-size: 1rem;
}

/* Batch 144 Rev 4 — Vermilion moved from feed cards to Autarch's Choice sidebar */
#autarchChoiceCard {
  border: 2px solid #CC3300 !important;
  background: rgba(204, 51, 0, 0.12) !important;
  box-shadow: 0 0 14px rgba(204, 51, 0, 0.25), inset 0 0 20px rgba(204, 51, 0, 0.05) !important;
}

.council-fav-star:hover {
  color: #e8c14c !important;
}

/* Badge icon in profile banner */
.profile-badge-icon {
  display: inline-block;
  vertical-align: middle;
  transition: transform 0.15s;
}

.profile-badge-icon:hover {
  transform: scale(1.15);
}

/* Batch 163 — Saint angel-wings badge. Wings flank the wrapped points-badge on
   both sides (wings alone when the Saint holds no points-badge). The right wing is
   the same image mirrored. Sized in em to scale with surrounding context. */
.saint-winged {
  gap: 0.05em;
}
/* Batch 247 D3 — .saint-wing / .saint-wing-left / .saint-wing-right REMOVED. N5 retired
   the two-piece angel-wings badge for the winged-crown (.saint-crown below); no element
   emits the wing classes anymore. The .saint-winged flex wrapper is retained — it now
   hosts the crown + tier icon (the two-badge tandem). */
/* Batch 247 N5 — Winged-crown saint badge. Em-scaled so it sits inline beside the
   points-tier badge (the two-badge tandem). Slightly wider than tall to fit the wings. */
.saint-crown {
  height: 1em;
  width: 1.25em;
  vertical-align: middle;
  object-fit: contain;
  filter: drop-shadow(0 0 1px rgba(244,196,48,0.45));
}

/* Rank delta in profile stat block */
.profile-stat-block .num {
  word-break: break-word;
}

/* Batch 59 — Prevent flash of login page on refresh */
.has-session #guestActions { display: none !important; }
/* Batch 259 — enrollment tabs are GUEST-ONLY (Autarch ruling): the same gate
   that hides guest chrome on login hides Join the Autocracy / Become a
   Civilian / Log In. display:none on the display:contents wrapper removes
   all three buttons at once. */
.has-session #enrollTabs { display: none !important; }
.has-session #heroWelcome { display: none !important; }
.has-session #userActions { display: flex !important; }
/* Batch 76 — Landing page gating: hide auth section + show nav when logged in */
.has-session #landingAuth { display: none !important; }
.has-session #mainNav { display: flex !important; }
/* Batch 76 — Hide nav + feed for unauthenticated (reinforced by JS) */
html:not(.has-session) #mainNav { display: none !important; }
html:not(.has-session) .feed-layout { display: none !important; }
/* Batch 258 — GUEST MODE (Autarch ruling): the Batch 76 gate above blanked the
   Guest Feed (inline display:'' defers to the !important rule). html.guest-mode,
   set by updateUI's logged-out branch, re-admits exactly the guest surfaces:
   the single all-posts feed and announcements. mainNav (Community/Discovery/
   Forum/Shop) and the forum/shop sections stay hidden for guests by design. */
html.guest-mode .feed-layout { display: grid !important; }
html.guest-mode #announcementBanner { display: block !important; }
/* Batch 150 Rev 4d — Gate all content sections behind has-session */
html:not(.has-session) #forumSection { display: none !important; }
html:not(.has-session) #shopSection { display: none !important; }
html:not(.has-session) #announcementBanner { display: none !important; }

/* ============================================================
   BATCH 66 — Story Patron System Styles
   ============================================================ */

.patron-tier-option {
  background: var(--ash-mid);
  transition: border-color 0.2s, background 0.2s;
}

.patron-tier-option:hover {
  border-color: var(--sepia) !important;
  background: var(--ash-dark);
}

/* Patron badge on story forum cards */
.patron-badge {
  display: inline-block;
  font-family: Cinzel, serif;
  font-size: 0.6rem;
  color: var(--sepia);
  border: 1px solid var(--sepia);
  border-radius: var(--radius);
  padding: 0.1rem 0.35rem;
  letter-spacing: 0.06em;
  margin-left: 0.3rem;
  vertical-align: middle;
}

/* Volume label in swipe dots */
#swipeDots span {
  transition: color 0.15s;
}

/* Add-volume dropdown positioning */
[id^="addVolumeDropdown-"] {
  background: var(--ash-dark);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 0.3rem 0;
  box-shadow: 0 4px 12px rgba(0,0,0,0.3);
}

[id^="addVolumeDropdown-"] div {
  padding: 0.5rem 0.8rem;
  font-size: 0.82rem;
  color: var(--parchment);
  cursor: pointer;
  font-family: inherit;
}

[id^="addVolumeDropdown-"] div:hover {
  background: var(--ash-mid);
  color: var(--sepia);
}

/* ============================================================
   BATCH 71 — Forum Points Integration, Poll Overhaul, DTP
   ============================================================
   No style changes. Poll voting UI uses inline styles in
   app.js. Report card category point labels use inline styles
   in _buildReportCardHtml. DTP violation subblock reuses
   existing .kv class for category badge styling.
   ============================================================ */

/* ============================================================
   BATCH 72 — Codebase Scan, Bug Fixes & Revisions
   ============================================================
   No style changes. All Batch 72 work is in app.js, db.js,
   egg.js, and README.md. Interaction button active states
   use existing .liked class (Rev 5).
   ============================================================ */

/* ============================================================
   BATCH 73 — Batch 72 Scan Fixes + Revisions
   ============================================================
   Rev 2 — Removed margin-left:auto; margin-right:auto from
   .gallery-feed-card. Discovery posts now left-align with
   .post-card in the community feed.
   ============================================================ */

/* ============================================================
   BATCH 74 — Batch 73 Scan Fixes + Poll Vote Feedback
   ============================================================
   No changes to this file.
   Scan fixes, notification fix, voters tab, fullscreen parity in app.js.
   SQL files corrected: supabase-auth-rls.sql, supabase-security-migration.sql.
   ============================================================ */

/* ============================================================
   BATCH 75 — Codebase Scan Fixes
   ============================================================
   No changes to this file.
   Scan fixes in app.js and db.js. Dead IVAI code removed. README file tree corrected.
   ============================================================ */

/* ============================================================
   BATCH 76 — Landing Page Gate + Split-Screen Auth
   ============================================================
   Rev 1 — .landing-auth-section, .landing-auth-inner, .landing-auth-panel,
     .landing-auth-heading, .landing-auth-divider styles added.
     Responsive: flex-direction column on mobile.
   Rev 2 — .hero-body max-width widened to 600px, line-height added.
   Rev 3 — has-session CSS rules: #landingAuth hidden, #mainNav shown.
     html:not(.has-session) hides #mainNav and .feed-layout.
   ============================================================ */

/* ============================================================
   BATCH 78 — Discovery Split-Screen + Trending
   ============================================================ */

.discovery-split {
  display: flex;
  gap: 1.5rem;
  align-items: flex-start;
}
.discovery-main {
  flex: 1;
  min-width: 0;
}
.discovery-trending-wrapper {
  width: 340px;
  flex-shrink: 0;
  position: sticky;
  top: 5rem;
  max-height: calc(100vh - 5.5rem);
  overflow-y: auto;
  overscroll-behavior: contain;
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  background: var(--ash-mid);
  scrollbar-width: thin;
  scrollbar-color: var(--border) transparent;
}
.discovery-trending-label {
  font-family: Cinzel, serif;
  font-size: 1rem;
  letter-spacing: 0.1em;
  color: var(--sepia);
  padding: 0.75rem 0.75rem 0.5rem;
  margin-bottom: 0.25rem;
  position: sticky;
  background: var(--ash-mid);
  z-index: 6;
  /* Batch 197 R5 — seal the top edge so scrolling posts never bleed through
     between the rounded wrapper border and this sticky label.
     Batch 201 — attempted to also cover the rounded corner shoulders by bleeding
     the label 1px past the wrapper's content box with negative side margins.
     Batch 202 (R1 re-fix) — that side-bleed was UNREACHABLE: the wrapper is
     overflow-y:auto, which computes overflow-x to auto as well, so the -1px side
     margins were clipped by the scroll container and never painted over the corner
     arcs. The reliable seal needs no bleed: the wrapper's own overflow clip rounds
     every scrolling card to the wrapper's shape (cards cannot paint into the corner
     arcs in the first place), and this label's matched top-corner radii + opaque
     --ash-mid fill + raised z-index cap the top band cleanly. Negative top margin
     retained at -1px ONLY to close the sub-pixel seam directly above the sticky
     (that axis is the scroll axis and tucks under the wrapper's top border, not
     clipped sideways).
     Batch 208 Item 2 — the -1px proved insufficient: the wrapper has a 1px solid
     border and the label sticks to top:0 (the CONTENT-box top, inside that border),
     leaving the border-width band above the label where a scrolling card's top edge
     flashes through before the label pins. Pin the label to top:-1px so it anchors at
     the border edge, and widen the upward bleed to -2px to cover the band. The top
     axis is the scroll axis (tucks under the wrapper top border, NOT clipped sideways
     like the abandoned side-bleed), so this paints reliably. */
  top: -1px;
  margin-top: -2px;
  border-top-left-radius: var(--radius-lg);
  border-top-right-radius: var(--radius-lg);
}
.discovery-trending-wrapper .post-card {
  margin-bottom: 0.5rem;
}

/* Batch 187 R3 — Trending refresh button. Idle: hollow / white-outline glyph on a
   transparent circle. Hover: sepia. Active (.trending-refreshing): filled dark
   circle, light glyph, continuous spin until the engagement re-pull resolves. */
.trending-refresh-btn {
  display: inline-flex; align-items: center; justify-content: center;
  width: 30px; height: 30px; flex-shrink: 0;
  border: 1px solid var(--parchment); border-radius: 50%;
  background: transparent; color: var(--parchment);
  cursor: pointer; padding: 0;
  transition: color 0.15s, border-color 0.15s, background 0.15s;
}
.trending-refresh-btn:hover { color: var(--sepia); border-color: var(--sepia); }
.trending-refresh-btn.trending-refreshing {
  background: var(--ink); color: var(--parchment); border-color: var(--ink);
  cursor: default;
}
.trending-refresh-btn.trending-refreshing svg { animation: trendingSpin 0.8s linear infinite; }
@keyframes trendingSpin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }

/* Batch 187 R2 — Load New Posts button. Hollow / sepia-outline pill, idle. Hover
   intensifies. Active (.discover-loading-new): glyph spins while the oldest unseen
   posts are admitted. */
.load-new-discover-btn {
  display: inline-flex; align-items: center; gap: 0.5rem;
  background: transparent; border: 1px solid var(--sepia); border-radius: 999px;
  padding: 0.45rem 1.3rem; cursor: pointer;
  color: var(--sepia); font-family: Cinzel, serif; font-size: 0.8rem; letter-spacing: 0.06em;
  transition: background 0.15s, color 0.15s, border-color 0.15s;
}
.load-new-discover-btn:hover { background: var(--sepia); color: var(--ink); }
/* Batch 193 R2 — Loading state: glyph spins AND the control glows gold until the
   batch resolves, then the class is removed and it returns to idle. Shared by the
   Discover and Community load-more controls. */
.load-new-discover-btn.discover-loading-new {
  color: var(--sepia); border-color: var(--sepia); cursor: default;
  box-shadow: 0 0 8px 1px var(--sepia), 0 0 16px 2px rgba(193, 154, 107, 0.45);
}
.load-new-discover-btn.discover-loading-new svg { animation: trendingSpin 0.8s linear infinite; }

/* Batch 267 (Autarch directive) — "Load New Forums" pill on the Forum Gallery
   header. Idle: black ground, gold border, gold text. Hover AND loading
   (.forums-loading-new): the box fills gold, text and glyph turn black. The
   glyph spins while the repull is in flight, then the pill returns to idle. */
.load-new-forums-btn {
  display: inline-flex; align-items: center; gap: 0.5rem;
  background: #000; border: 1px solid var(--gold); border-radius: 999px;
  padding: 0.45rem 1.3rem; cursor: pointer;
  color: var(--gold); font-family: Cinzel, serif; font-size: 0.8rem; letter-spacing: 0.06em;
  transition: background 0.15s, color 0.15s;
}
.load-new-forums-btn:hover,
.load-new-forums-btn.forums-loading-new { background: var(--gold); color: #000; }
.load-new-forums-btn.forums-loading-new { cursor: default; }
.load-new-forums-btn.forums-loading-new svg { animation: trendingSpin 0.8s linear infinite; }

/* Batch 267 (Autarch directive) — toggle hover descriptors. A site-styled card
   that appears to the LEFT of a floating toggle after a 3-second hover: dark
   ground, gold border, short descriptor line. Replaces the native browser
   title tooltips on the toggle tokens (stripped in index.html so the two can
   never double-show). Positioned by _initToggleDescriptors in app.js. */
.toggle-descriptor {
  position: fixed; z-index: 99999;
  background: var(--ash, #2a2622); border: 1px solid var(--gold);
  border-radius: 6px; padding: 0.35rem 0.7rem;
  color: var(--parchment, #e8dcc8); font-family: Cinzel, serif;
  font-size: 0.72rem; letter-spacing: 0.05em; white-space: nowrap;
  box-shadow: 0 4px 18px rgba(0,0,0,0.55); pointer-events: none;
}

/* Batch 193 R5 — Violation checklist on the report investigation modal. A horizontal
   row of toggle "checkboxes", one per discovered code. Box is empty (outline only)
   when unticked; fills SOLID WHITE when ticked. B.I.T.C.H is tinted red to signal it
   is a standalone ban override. */
.violation-check-row {
  display: flex; flex-wrap: wrap; gap: 0.5rem; align-items: center; margin-top: 0.4rem;
}
.violation-check {
  display: inline-flex; align-items: center; gap: 0.35rem;
  background: transparent; border: 1px solid var(--border-strong); border-radius: var(--radius);
  padding: 0.3rem 0.6rem; cursor: pointer;
  color: var(--text-muted); font-family: Cinzel, serif; font-size: 0.72rem; letter-spacing: 0.05em;
  transition: border-color 0.15s, color 0.15s;
}
.violation-check:hover { border-color: var(--sepia); color: var(--parchment); }
.violation-check-box {
  display: inline-block; width: 12px; height: 12px; border-radius: 2px;
  border: 1.5px solid var(--text-muted); background: transparent; flex-shrink: 0;
  transition: background 0.12s, border-color 0.12s;
}
.violation-check.checked .violation-check-box { background: #ffffff; border-color: #ffffff; }
.violation-check.checked { color: var(--parchment); border-color: var(--parchment); }
.violation-check-bitch { border-color: rgba(160,30,30,0.5); color: #e06060; }
.violation-check-bitch:hover { border-color: #e06060; color: #e06060; }
.violation-check-bitch.checked { border-color: #e06060; color: #e06060; }
.violation-check-bitch.checked .violation-check-box { background: #e06060; border-color: #e06060; }
.discovery-trending-wrapper .post-card-image {
  max-height: 240px;
}
@media (max-width: 768px) {
  .discovery-split { flex-direction: column; }
  .discovery-trending-wrapper {
    width: 100%;
    position: static;
    max-height: none;
    border-top: 1px solid var(--border);
    padding-top: 1rem;
    margin-top: 1rem;
  }
}

/* Batch 84 Rev 3 — Reverted Batch 83 Rev 1 (discovery independent scroll).
   The @media (min-width:769px) block locked #gallerySection to viewport height
   and broke main feed scroll. Base CSS already provides correct behavior:
   .discovery-trending-wrapper: sticky + overflow-y:auto (independent scroll).
   .discovery-trending-label: sticky top:0 inside wrapper (always visible). */

/* ============================================================
   ============================================================
   No changes to this file.
   Scan report fixes in app.js, db.js, council.js, egg.js.
   ============================================================ */

/* ============================================================
   REVISION LOG — BATCH 78 (FG Opus Claude)
   ============================================================
   Rev 3 — Discovery split-screen styles: .discovery-split,
     .discovery-main, .discovery-trending-wrapper (sticky),
     .discovery-trending-label, .trending-card (compact cards),
     .trending-card-img (aspect-ratio 3/4, object-fit contain).
     Responsive: stacks vertically on mobile.
   ============================================================ */

/* ============================================================
   REVISION LOG — BATCH 79 (FG Opus Claude)
   ============================================================
   Rev 5 — Trending section redesign: compact card classes removed
     (.trending-card, .trending-card-img, .trending-card-body,
     .trending-card-author, .trending-card-text, .trending-card-stats).
     Replaced by standard .post-card layout via _renderPostAsFeedCard.
     .discovery-trending-wrapper: 280px → 340px, panel border +
     border-radius + ash-mid background, overscroll-behavior: contain
     for scroll independence. .post-card-image max-height scoped
     inside wrapper.
   ============================================================ */

/* ============================================================
   REVISION LOG — BATCH 80 (FG Opus Claude)
   ============================================================
   Scan cleanup — 3 dead CSS rules removed:
     .hero-cta (Batch 76 removed hero CTA buttons),
     .fav-book-info .fav-title / .fav-series (books pipeline dead),
     .tier-card.selected (zero references).
   Rev 2 — .discovery-trending-label: position sticky, top 0,
     opaque background (ash-mid), z-index 2. Scrolling content
     passes behind the label.
   ============================================================ */

/* ============================================================
   REVISION LOG — BATCH 81 (FG Opus Claude)
   ============================================================
   No changes to this file.
   _checkFsdExpiry FM/admin guard + _closureReconciled
   persistence fix in app.js.
   ============================================================ */

/* ============================================================
   REVISION LOG — BATCH 82 (FG Opus Claude)
   ============================================================
   Rev 10 — REVERTED. Fixed-height discovery-split broke layout.
     Original CSS restored: .discovery-split flex-start,
     .discovery-main no overflow, .discovery-trending-wrapper
     position:sticky with max-height + overflow-y:auto.
     Mobile breakpoint restored with position:static.
   ============================================================ */

/* ============================================================
   REVISION LOG — BATCH 83 (FG Opus Claude)
   ============================================================
   Rev 1 — Discovery independent scroll: #gallerySection becomes
     flex column (height: calc(100vh - 72px), overflow: hidden).
     .discovery-main gets overflow-y:auto. .discovery-trending-wrapper
     drops position:sticky. Wrapped in @media (min-width: 769px).
   Rev 9 — --ash-dark: #1e1b17 defined in :root. Previously used
     in 6 sites (app.js + styles.css) but never declared.
   Rev 11 — 12 dead CSS rules removed: .profile-edit-btn,
     .profile-edit-area, .profile-edit-area:focus, .profile-grid
     media query, .user-popup-comment-row, .user-popup-comment-date,
     .status-orange, .status-yellow, .forum-card-lore .forum-card-title,
     .forum-card-lore .forum-card-preview, .fl-deactivate-btn from
     combined selector, .top-nav from admin-fullscreen-mode selector.
   ============================================================ */

/* ============================================================
   REVISION LOG — BATCH 84 (FG Opus Claude)
   ============================================================
   Rev 3 — Reverted Batch 83 Rev 1 discovery CSS.
   Removed @media (min-width:769px) block that locked
   #gallerySection to viewport height with overflow:hidden.
   Base CSS already provides correct behavior:
   .discovery-trending-wrapper: sticky + overflow-y:auto.
   .discovery-trending-label: sticky top:0 inside wrapper.
   ============================================================ */

/* ============================================================
   REVISION LOG — BATCH 85 (FG Opus Claude)
   ============================================================
   Rev 1 — Trending label sticky offset fix.
   .discovery-trending-wrapper top: 1rem → 5rem,
   max-height: calc(100vh - 2rem) → calc(100vh - 5.5rem).
   Clears the 72px sticky site header so "TRENDING" label
   remains visible during scroll.
   ============================================================

   REVISION LOG — BATCH 86 (FG Opus Claude)
   ============================================================
   No changes to this file.
   8 audit fixes + 2 revisions in app.js. Auth hardening in
   get-ip and kemi-scan edge functions. Trending 24-hour window,
   initSite query resilience.
   ============================================================

   REVISION LOG — BATCH 87 (FG Opus Claude)
   ============================================================
   No changes to this file.
   6 fixes: revision log corruption (council/sovereign/egg/kemi-config),
   trending engagement filter + dead code (app.js), saveDailySnapshot
   409 fix (db.js), trending try/catch (app.js), duplicate gallery
   card IDs fixed in community + profile feeds (app.js).
   ============================================================

   REVISION LOG — BATCH 88 (FG Opus Claude)
   ============================================================
   Fix 3 — Dead .gallery-card-img CSS removed.
     .gallery-card-img base rule and trending wrapper scoped
     extension removed. Class no longer used after gallery branch
     in _renderPostAsFeedCard switched to .gallery-feed-card class
     with Discover-matched image inline styles.
   Also: 2 fixes in app.js (gallery card sizing restored via
   .gallery-feed-card class, comment button interaction restored).
   ============================================================

   REVISION LOG — BATCH 89 (FG Opus Claude)
   ============================================================
   No changes to this file.
   Trending 24-hour engagement cycle, story report card restructure,
   story notification verification, volume cap in app.js. Engagement
   log CRUD in db.js. SQL migration required for engagement_log table.
   ============================================================ */

/* ============================================================
   REVISION LOG — BATCH 90 (FG Opus Claude)
   ============================================================
   No changes to this file.
   FM closure notification gap fixed (5 paths in app.js).
   Community feed following data preserved across admin user reload.
   DB_GALLERY refreshed in 60-second sync loop.
   ============================================================ */

/* ============================================================
   REVISION LOG — BATCH 91 (FG Opus Claude)
   ============================================================
   No changes to this file.
   11 revisions in app.js: inline interactions, forum kind labels,
   report card fixes, image rendering, rating selector persistence,
   debate/story modal refresh, quote limit 200->500.
   Membership card removed from index.html.
   ============================================================ */

/* ============================================================
   REVISION LOG — BATCH 92 (FG Opus Claude)
   ============================================================
   Fix 6 — .violation-tag.ivai CSS rule removed (dead code;
   merchant protocol removed Batch 70, nothing sets class 'ivai').
   ============================================================ */

/* ============================================================
   REVISION LOG — BATCH 93 (FG Opus Claude)
   ============================================================
   No changes to this file.
   Multi-page forum restructure, debate comment rendering
   restoration, polling re-render, session resilience in app.js.
   ============================================================ */

/* ============================================================
   REVISION LOG — BATCH 94 (FG Opus Claude)
   ============================================================
   No changes to this file.
   Fullscreen comment parity fixes in app.js.
   ============================================================ */

/* ============================================================
   REVISION LOG — BATCH 95 (FG Opus Claude)
   ============================================================
   Finding 6 — Stale architecture comment corrected: "sovereign/"
   path refs updated to "egg.js and sovereign.js", session
   persistence updated from "localStorage dms_session_userId"
   to "Supabase Auth JWT".
   ============================================================ */

/* ============================================================
   REVISION LOG — BATCH 96 (FG Opus Claude)
   ============================================================
   No changes to this file.
   Batch 93 page structure reversion, _currentSwipePage per-post
   scoping, _storyRatings deprecation — all in app.js.
   ============================================================ */

/* ============================================================
   REVISION LOG — BATCH 97 (FG Opus Claude)
   ============================================================
   No changes to this file.
   Per-round debate sidebar, scroll preservation, init flow fix,
   session comment corrections, _storyRatings cleanup — app.js/db.js.
   ============================================================ */

/* ============================================================
   REVISION LOG — BATCH 98 (FG Opus Claude)
   ============================================================
   No changes to this file.
   5 audit fixes in app.js and db.js.
   ============================================================ */

/* ============================================================
   REVISION LOG — BATCH 99 (FG Opus Claude)
   ============================================================
   No changes to this file.
   ============================================================ */

/* ============================================================
   REVISION LOG — BATCH 100 (FG Opus Claude)
   ============================================================
   No changes to this file.
   ============================================================ */

/* ============================================================
   REVISION LOG — BATCH 101 (FG Opus Claude)
   ============================================================
   No changes to this file.
   ============================================================ */

/* ============================================================
   REVISION LOG — BATCH 102 (FG Opus Claude)
   ============================================================
   No changes to this file.
   ============================================================ */

/* ============================================================
   REVISION LOG — BATCH 103 (FG Opus Claude)
   No code changes. Revision log only.
   ============================================================ */

/* ============================================================
   REVISION LOG — BATCH 104 (FG Opus Claude)
   No code changes. Revision log only.
   ============================================================ */

/* ============================================================
   REVISION LOG — BATCH 105 (FG Opus Claude)
   No code changes. Revision log only.
   ============================================================ */

/* ============================================================
   REVISION LOG — BATCH 106 (FG Opus Claude)
   No code changes. Revision log only.
   ============================================================ */

/* ============================================================
   REVISION LOG — BATCH 107 (FG Opus Claude)
   Rev 5 — Removed hardcoded color:var(--parchment) from
     .profile-page-username CSS class. Username color now
     controlled entirely by inline getUserColor() style.
   ============================================================ */

/* ============================================================
   REVISION LOG — BATCH 109 (FG Opus Claude)
   No code changes. Revision log only.
   ============================================================ */
/* ============================================================
   REVISION LOG — BATCH 110 (FG Opus Claude)
   No code changes. Revision log only.
   ============================================================ */
/* ============================================================
   REVISION LOG — BATCH 111 (FG Opus Claude)
   No code changes. Revision log only.
   ============================================================ */
/* ============================================================
   REVISION LOG — BATCH 112 (FG Opus Claude)
   No code changes. Revision log only.
   ============================================================ */
/* ============================================================
   REVISION LOG — BATCH 113 (FG Opus Claude)
   No code changes. Revision log only.
   Points/Rewards settings, trending Load More spin, new forum
   categories, and 2 audit fixes all in app.js + index.html.
   ============================================================ */

/* ============================================================
   REVISION LOG — BATCH 114 (FG Opus Claude)
   No code changes. 3 dead variables removed from app.js.
   Checkbox labels aligned in index.html.
   ============================================================ */

/* ============================================================
   REVISION LOG — BATCH 115 (FG Opus Claude)
   Fix 6 — 3 unused CSS variables removed from :root.
     --gold-light (#f0c840), --white (#fdf8f0), --text-body
     (#2a2620) defined but never referenced in any var() call
     across styles.css, app.js, or index.html.
   CSS bracket balance: 487/487 (correcting v83/v84 claim of
     478/478 — v82's 487/487 was correct all along).
   ============================================================ */

/* ============================================================
   REVISION LOG — BATCH 116 (FG Opus Claude)
   No code changes to styles.css.
   Discussion Forums (new forum kind) added to app.js.
   Open House expand/collapse added to report card.
   ============================================================ */

/* ============================================================
   REVISION LOG — BATCH 117 (FG Opus Claude)
   No code changes to styles.css.
   7 audit fixes in app.js for Batch 116 discussion forum integration.
   ============================================================ */

/* REVISION LOG — BATCH 118 (FG Opus Claude)
   No code changes. Revision log only.
   Rev 1: loadGalleryFeed decoupled from init Promise.all (app.js).
   Rev 2: Post-publish Add Documents for all forum kinds (app.js).
   Rev 3: Discussion FM panel sidebar Key Conversations tab (app.js).
*/

/* REVISION LOG — BATCH 119 (FG Opus Claude)
   No code changes. Revision log only.
   5 audit fixes in app.js: modal refresh, doc count, XSS escaping,
   behavioral documentation, stale comment update.
*/

/* ============================================================
   REVISION LOG — BATCH 120 (FG Opus Claude)
   No code changes. Revision log only.
   3 revisions in app.js: discussion comment input fix,
   discussion open reply access, discussion point dynamics.
*/

/* ============================================================
   REVISION LOG — BATCH 121 (FG Opus Claude)
   No code changes. Revision log only.
   5 audit fixes in app.js: discussion point qualification
   (savedPosts → followedForums), discussion report card branch,
   MAX_ACTIVE_FORUMS ×6, document URL _safeUrl, dead label branch.
   ============================================================ */

/* ============================================================
   REVISION LOG — BATCH 122 (FG Opus Claude)
   No code changes. Revision log only.
   2 audit fixes in app.js: dead 'Description' label branches
   (6 instances), Points/Rewards settings label 'save' → 'follow'.
   ============================================================ */

/* ============================================================
   REVISION LOG — BATCH 123 (FG Opus Claude)
   No code changes. Revision log only.
   5 revisions in app.js: Discussion menu Pin→Highlight,
   reply input + reply chain + focus preservation across poll refresh,
   community feed reply preservation, Key Conversation popup fix.
   ============================================================ */

/* ============================================================
   REVISION LOG — BATCH 124 (FG Opus Claude)
   No code changes. Revision log only.
   3 audit fixes in app.js: dead local variables removed from
   popup function, forum follow notification text "saves"→"follows",
   stale code comment "save"→"follow".
   ============================================================ */

/* ============================================================
   REVISION LOG — BATCH 125 (FG Opus Claude)
   No code changes. Revision log only.
   3 revisions in app.js: reply input cleared before re-render to
   prevent save/restore persistence, trending card click handlers
   added (openGalleryOverlay/openPost), discussion sidebar flash
   on vote eliminated via early-return.
   ============================================================ */

/* ============================================================
   REVISION LOG — BATCH 126 (FG Opus Claude)
   No code changes. Revision log only.
   2 audit fixes in app.js: trending delegation check added img to
   prevent double modal on gallery image click; discussion report
   card qualification text "save (follow)" to "follow".

   REVISION LOG — BATCH 127 (FG Opus Claude)
   Rev 4 — overscroll-behavior: contain added to .modal-box.
   Prevents scroll bleed-through from any modal to the main page.
   Other revisions in app.js: scroll fix, ban enforcement, discussion
   panel continuation navigation, trending popup-only cards.
   ============================================================ */

/* ============================================================
   REVISION LOG — BATCH 128 (FG Opus Claude)
   No code changes. Revision log only.
   3 audit fixes in app.js: fullscreen ban filter (6 fix points),
   Key Conversation popup/sidebar ban filter, notification ban filter.
   ============================================================ */

/* ============================================================
   REVISION LOG — BATCH 129 (FG Opus Claude)
   Rev 1 — overflow: hidden added to .modal-overlay. Prevents
   scroll bleed-through from any modal to the main page body.
   Other revisions in app.js: openGalleryOverlay fix (3 sites),
   token color username restoration in _applyColorSchemeToPage.
   ============================================================ */

/* ============================================================
   REVISION LOG — BATCH 130 (FG Opus Claude)
   No code changes. Revision log only. Batch 128 header format
   corrected to established "REVISION LOG — BATCH 128" header.
   Fix 1 in app.js: openRepostModal → openQuoteRepost.
   ============================================================ */

/*  ============================================================
   REVISION LOG — BATCH 131 (FG Opus Claude)
   ============================================================
   No code changes. Revision log only.
   Rev 1: Polling cycle cleanup (app.js). Rev 2: SQL migration.
   Rev 3-4: forum_continuation handler + icon (app.js).
   Rev 5: Discussion report card redesign (app.js).
   Rev 6: _reconcileForumClosure guard fix (app.js).
   ============================================================ */

/*  ============================================================
   REVISION LOG — BATCH 132 (FG Opus Claude)
   ============================================================
   No code changes. Revision log only.
   Fix 1: Dead function removal (app.js). Fix 2: Discussion point
   award logic aligned with report card (app.js). Fix 3: _checkFsdExpiry
   restored to init path (app.js). Fix 4: _checkDebateNoticeTimeouts
   restored to init path (app.js). Fix 5: Documentation fix.
   ============================================================

   ============================================================
   REVISION LOG — BATCH 133 (FG Opus Claude)
   ============================================================
   No code changes. Revision log only.
   Rev 1: Retroactive forum_continuation notifications (app.js).
   No styles.css changes.
   ============================================================ */

/* ============================================================
   REVISION LOG — BATCH 134 (FG Opus Claude)
   ============================================================
   No style changes. Revision log only.
   Rev 1-5: Continuation notification dedup, reconciliation point awards,
   init-path guards, polling cycle purity (app.js). No styles.css changes.
   ============================================================ */
/* ============================================================
   REVISION LOG — BATCH 135 (FG Opus Claude)
   Rev 3 — Active community card:
     .feed-sidebar: position:sticky, top:5rem, max-height,
     overflow-y:auto, overscroll-behavior:contain. Independent
     scroll with thin scrollbar. Responsive override at 1024px:
     position:static, max-height:none.
     #activeMemberList scrollbar styling added.
     .member-tier rule removed (tier labels no longer displayed).
   ============================================================ */

/* REVISION LOG — BATCH 136
   No code changes. Revision log only. */
/* REVISION LOG — BATCH 137
   No code changes. autarchPinned infrastructure restored in app.js.
   Dead openNewBookListing removed from app.js. */

/* ============================================================
   REVISION LOG — BATCH 138 (FG Opus Claude)
   ============================================================
   No changes to this file.
   Autarch's Choice rework, interaction buttons, book sub-category,
   trending fallback, and sidebar info in app.js/db.js/index.html.
   ============================================================ */

/* REVISION LOG — BATCH 139 (FG Opus Claude)
   No code changes. Revision log only.
*/

/* REVISION LOG — BATCH 140 (FG Opus Claude) */
/* No code changes. Revision log only. */

/* REVISION LOG — BATCH 141 (FG Opus Claude)
   No changes to this file. _safeUrl completion, _esc defense-in-depth,
   announcement banner ordering fix in app.js.
*/

/* REVISION LOG — BATCH 142 (FG Opus Claude) */
/* No code changes. Revision log only. */
/* See app.js for Batch 142: 4 revisions. */

/* REVISION LOG — BATCH 143 (FG Opus Claude) */
/* No code changes. Revision log only. */
/* app.js: Fix 1 (_syncBodyOverflow), Fix 2 (dead openNewPostSelector removed), */
/*   Fix 3 (dead submitBookListing removed), Fix 4 (trending preload sort aligned). */
/* index.html: dead newPostSelectorModal + newBookListingModal HTML removed. */

/* REVISION LOG — BATCH 144 (FG Opus Claude) */
/* Rev 2 — .sticky-modal-bar and .sticky-modal-header CSS classes for opaque */
/*   sticky sections. background:var(--ash-dark) !important. */
/* Rev 4 — .autarch-choice-card + .post-tag sub-rule replaced with */
/*   #autarchChoiceCard vermilion styling. */
/* Rev 6 — .forum-square-card: position:relative added for A/star button. */
/* REVISION LOG — BATCH 145 (FG Opus Claude) */
/* No code changes. CSS 495/495 unchanged. See app.js revision log. */
/* REVISION LOG — BATCH 146 (FG Opus Claude) */
/* Fix 1 — .sticky-modal-bar background changed from var(--ash-dark) to var(--ash). */
/*   .sticky-modal-header background changed from var(--ash-dark) to var(--ash). */
/*   .comment-input-area margin-top reduced from 1rem to 0.25rem. */
/*   Bars remain opaque (Rule 2.28 satisfied); color now matches modal background. */
/* REVISION LOG — BATCH 147 (FG Opus Claude) */
/* No changes to styles.css. Audit-driven fixes in app.js only. */

/* ============================================================ */
/* REVISION LOG — BATCH 148 (FG Opus Claude) */
/* ============================================================ */
/* Rev 1 — .sticky-modal-bar padding-bottom 0.5rem → 2.5rem. */
/*   Full-coverage opacity: bar now covers modal-box 2rem bottom */
/*   padding zone. Background unchanged (var(--ash) !important). */
/* ============================================================ */

/* REVISION LOG — BATCH 149 (FG Opus Claude) */
/* No code changes. 7 audit-driven fixes in app.js only. */

/* REVISION LOG — BATCH 150 (FG Opus Claude) */
/* Rev 2 — .sticky-modal-bar and .sticky-modal-header: added */
/*   z-index:2 and transform:translateZ(0) for compositing layer. */
/* Rev 4d — CSS gate: html:not(.has-session) rules for */
/*   #forumSection, #shopSection, #announcementBanner. */
/* Rev 6 — .brand-name font-size 1.1rem→1.6rem, font-weight:700. */

/* REVISION LOG — BATCH 151 (FG Opus Claude) */
/* Fix 1 — Dead .brand-sub CSS class removed. HTML element removed */
/*   Batch 150 Rev 6. CSS bracket balance: 497/497. */

/* ============================================================ */
/* REVISION LOG — BATCH 152 (FG Opus Claude) */
/* Rev 1 — .brand-sub CSS class restored. .brand-full/.brand-short */
/*   containers added. html.has-session toggle rules for conditional */
/*   header brand (logged-out: full, logged-in: DMS.). */
/*   CSS bracket balance: 503/503. */
/* ============================================================ */

/* ============================================================ */
/* REVISION LOG — BATCH 153 (FG Opus Claude) */
/* ============================================================ */
/* No code changes to this file. Batch 153: 4 audit-driven fixes */
/* in app.js — video URL domain allowlist, video caption keyword */
/* scanning, video rendering in 5 additional gallery contexts, */
/* interaction count consistency in 2 contexts. */
/* ============================================================ */

/* ============================================================
   REVISION LOG — BATCH 154 (FG Opus Claude)
   ============================================================
   No code changes to this file. Image height constraints applied
   via inline styles in app.js (max-height:500px;object-fit:contain).
   CSS bracket balance: 503/503 unchanged.
   ============================================================ */

/* REVISION LOG — BATCH 155 (FG Opus Claude) */
/* No code changes. CSS bracket balance: 503/503. */

/* REVISION LOG — BATCH 156 (FG Opus Claude) */
/* No code changes to this file. CSS bracket balance: 503/503. */
/* Batch 156: 5 fixes in app.js — AC sidebar video rendering, sticky */
/* comment bar compositing, video playback state management (3 sub-fixes), */
/* gallery image letterboxing, performance (DB_USER_MAP O(1) lookups). */

/* Batch 157 — No code changes. Revision log only. */

/* Batch 158 — No code changes to this file.
   Rev 1: Gallery image rendering restored (app.js only).
   Rev 2: RLS policy drop (SQL, no file changes).
   Rev 3: Session lock refresh redirect (app.js only). */

/* REVISION LOG — BATCH 159 (FG Opus Claude) */
/* Fix 3 — 27 JS-style (//) comments from Batches 149-153 and 157 */
/*   converted to proper CSS comments. Zero remaining // comments. */
/*   CSS bracket balance: 503/503 (unchanged). */

/* ============================================================
   REVISION LOG — BATCH 160 (FG Opus Claude)
   ============================================================
   Rev 3: .post-card-image-wrap max-height changed from 500px
     to 600px for forum post images in community feed. Forum tab
     rendering unchanged. Gallery/discover images use inline
     aspect-ratio:1/1 square format (app.js changes).
   ============================================================ */

/* ============================================================
   REVISION LOG — BATCH 161 (FG Opus Claude)
   ============================================================
   No code changes to this file.
   See app.js for Batch 161: 6 audit-driven fixes (crop tool
   listener leak, comment max-height consistency, CORS canvas
   try/catch, profile feed block filter, notification panel
   block filter).
   CSS bracket balance: 503/503 (unchanged).
   ============================================================ */

/* ============================================================
   BATCH 162 — REVISION LOG (FG Opus Claude)
   ============================================================
   No code changes. Revision log entry only.
   ============================================================ */

/* ============================================================
   REVISION LOG — BATCH 163 (FG Opus Claude)
   ============================================================
   Added Saint angel-wings badge styling: .saint-winged (inline-flex wrapper),
   .saint-wing (em-scaled gold wing image), .saint-wing-left / .saint-wing-right
   (the right wing mirrored via scaleX(-1)). Wings flank the wrapped points-badge.
   CSS bracket balance: 507/507.
   ============================================================ */

/* ============================================================
   REVISION LOG — BATCH 164 (FG Opus Claude)
   No code changes to this file. Batch 164 (privilege restructure + audit
   Findings 2 and 8) is in app.js, db.js, and the SQL migration. CSS 507/507.
   ============================================================ */

/* ============================================================
   REVISION LOG — BATCH 165 (FG Opus Claude)
   No code changes to this file. Batch 165 (audit-driven fixes: Problem 2/3/4/5)
   lives in app.js, db.js, and supabase-batch165-migration.sql. A1 struck by the
   Autarch. CSS 507/507.
   ============================================================ */

/* ============================================================
   REVISION LOG — BATCH 166 (FG Opus Claude)
   Added .magisterium-universal-card (+ .post-tag variant): ultraviolet tint for a
   Magisterium member's deliberate universal post. Mirrors the council-favorite-card
   idiom. Applied via renderPostCard (category 'universal' + majesty author). The
   Saint "St." honorific (Batch 166) is text-only in app.js — no CSS. CSS 509/509.
   ============================================================ */

/* ============================================================
   REVISION LOG — BATCH 167 (FG Opus Claude)
   Added .forum-kind-tabs: the forum kind-filter tab bar made sticky + fully
   opaque (Rule 2.28). top:72px (flush beneath the 72px .site-header), opaque
   --ink background (page surface — not translucent/darker, Batch 146),
   transform:translateZ(0) compositing layer (Batch 150). Added #forumSection
   position:relative;z-index:1 so scrolled cards resolve below the bar's z-index:2
   (Batch 156). Header height stable across media queries (no responsive gap).
   Two new rules added: CSS 509/509 -> 511/511. No other CSS touched this batch.
   ============================================================ */


/* ============================================================
   REVISION LOG — BATCH 168 (FG Opus Claude)
   No code changes to this file. Batch 168 (fixes for the eight Batch 167 audit
   findings) is app.js + SQL only. CSS bracket balance unchanged at 511/511.
   ============================================================ */

/* ============================================================
   REVISION LOG — BATCH 169 (FG Opus Claude)
   REV 1 — Added the Collective Board two-column admin workspace styles:
   .admin-workspace (flex, side-by-side at all widths), .admin-collective-board
   (clamp(280px,30vw,420px), collapsible), .admin-tab-column, .admin-board-
   collapse-btn, .admin-board-spine (+ -arrow / -label, vertical writing-mode),
   and the .collapsed state (shrinks to spine, hides content + chevron). No
   responsive stacking — horizontal collapse is the small-screen affordance.
   12 rules added. CSS bracket balance 511/511 -> 523/523.
   REV 2 — No CSS changes for the membership-wording revision.
   ============================================================ */

/* ============================================================
   Batch 170 — No change to this file. Audit fixes were F-1
   (award_points_rpc whitelist completeness, in
   supabase-security-migration.sql) and F-2 (a Collective Board
   comment correction in app.js). No CSS changed. Bracket balance
   unchanged at 523/523.
   ============================================================ */

/* ============================================================
   Batch 171 — No change to this file. The Rev 1–5 sweep is in
   app.js / council.js / kemi-index.ts. Rev 3 (Reports section) and
   Rev 4 (relocated Applications card) reuse existing classes
   (.admin-section, .admin-scrollable-window, .violation-row,
   .violation-tag, .admin-metric-card, .stat-grid) — no new CSS,
   no rule removed. Bracket balance unchanged at 523/523.
   ============================================================ */

/* ============================================================
   Batch 172 — No change to this file. Audit fixes A–G touched app.js,
   council.js, index.html, kemi-index.ts, and supabase-security-migration.sql.
   Finding C removed a dead modal's markup (index.html) but it reused existing
   classes, so no CSS rule was orphaned or added. Bracket balance unchanged
   at 523/523.
   ============================================================ */

/* ============================================================
   Batch 173 — No change to this file. The Metra-Expanse + dead-composer
   teardown removed JS/markup only; the .book-card and comment classes the
   Metra cluster had reused remain in use by live features (shop/books,
   gallery/forum comments), so no CSS rule was orphaned. Bracket balance
   unchanged at 523/523.
   ============================================================ */

/* ============================================================
   Batch 174 — Post-card expansion fix (Autarch directive).
   Symptom: opening a post's three-dot (.post-menu) appeared to grow the
   post card. Root: .post-menu is position:absolute, but its nearest
   positioned ancestor was .post-card; with .post-card no longer clipping
   (overflow:hidden removed in an earlier pass) the tall dropdown painted
   past the short card's lower border, reading as expansion.
   Fix: added position:relative to .post-menu-btn so the absolute .post-menu
   anchors to the small kebab BUTTON and overlays as a true floating dropdown
   rather than appearing to extend the card body. Applied via the shared class,
   so it is retroactive across all 20 .post-menu render sites (Rule 2.21). The
   opened-post path already wraps button+menu in its own absolute container, so
   re-anchoring to the button is consistent there too.
   (An initial flex-wrap hypothesis was applied then fully reverted on
   re-analysis before the correct fix — no speculative rule left behind.)
   One declaration added; bracket balance unchanged at 523/523.
   ============================================================ */

/* ============================================================
   Batch 175 — No styles.css change. R1 report-persistence (db.js
   Prefer:return=minimal on kemi_flags insert), R2 dossier scroll-
   restore on the real .modal-box container (app.js), R3 Site Stats
   real-time after a verdict (app.js), R4 duplicate-correspondence
   guard (app.js), R5 commission 10% (app.js + supabase-patron-
   migration.sql), R6 patron tiers 2.75/3.50/4.25/5.00 (app.js) are
   logic/data only. Verified no stylesheet impact. Bracket balance
   unchanged at 523/523. No new rule established.
   ============================================================ */
/* ============================================================
   Batch 176 — No styles.css change. Tip-commission split is logic/data in
   app.js (_submitTip) and db.js (createTip) + a tips-table SQL migration.
   Bracket balance unchanged at 523/523. Rule 2.32 established (Outstanding-
   list discipline). Set is now 2.1–2.32.
   ============================================================ */
/* ============================================================
   REVISION LOG — BATCH 177 (FG Opus Claude)
   No styles.css change. The four Batch 177 defect fixes are app.js logic; the
   Defect 4 CODE FAULT panel uses inline styles, adding no new selectors.
   Bracket balance unchanged at 523/523. Set remains 2.1–2.32.
   ============================================================ */

/* ============================================================
   REVISION LOG — BATCH 178 (Audit-fix pass)
   No change to styles.css. Audit fixes are in app.js / council.js / batch169
   SQL. CSS bracket balance unchanged. Set remains 2.1–2.32.
   ============================================================ */

/* ============================================================
   REVISION LOG — BATCH 179
   No change to styles.css. Batch 179 changes are app.js / db.js / council.js
   logic; the Autarch Delete menu entry reuses existing .post-menu styling.
   CSS bracket balance unchanged (523/523). Set remains 2.1-2.32.
   ============================================================ */

/* ============================================================
   Batch 180 — No change to styles.css. Post-audit fixes (F1-F4) are app.js /
   db.js logic; the trending gallery Delete menu entry reuses existing .post-menu
   styling. CSS bracket balance unchanged (523/523). Set remains 2.1-2.32.
   ============================================================ */

/* ============================================================
   Batch 181 — No change to styles.css. All five Batch 181 changes (R1 image-post
   Delete gates, R2 verdict-commit scoping, R3 dossier scroll, R4 overlay z-index,
   R5 admin-panel redesign) are app.js logic; the added Delete entries and the
   library filter menu reuse existing .post-menu styling and overlay elevation is
   set inline at runtime. CSS bracket balance unchanged (523/523). Set remains 2.1-2.32.
   ============================================================ */

/* ============================================================
   Batch 182 — No change to styles.css. Post-audit cleanup (dead z-index writes,
   dead vars/function removal) is app.js logic; the SECURITY DEFINER search_path
   pin is in supabase-patron-migration.sql. No styling change. CSS bracket balance
   unchanged (523/523). Set remains 2.1-2.32.
   ============================================================ */

/* ============================================================
   Batch 183 — No change to styles.css. The Batch 183 revisions are app.js logic;
   the report-modal verdict apparatus reuses existing classes (verdict-gavel-btn,
   verdict-selected, btn-primary/btn-ghost/btn-danger, user-popup-section-title).
   No styling change. CSS bracket balance unchanged (523/523). Set remains 2.1-2.32.
   ============================================================ */

/* Batch 184 — No change to styles.css. Batch 184 fixed two app.js/db.js defects (F1 Death Row population gap; F2 forum post/comment edit + delete handlers); F2 reuses existing comment-edit and button classes. Log entry per Rule 2.5. Set remains 2.1-2.32. */

/* Batch 185 — No change to styles.css. Batch 185 R5 (History section + Re-Open Case modal) reuses existing classes only: violation-subblock, violation-tag, violation-preview-box, admin-scrollable-window, user-popup-section-title, verdict-gavel-btn, verdict-selected, modal-overlay/box/close/title, btn-ghost/danger/primary — all verified present. R1-R4 add no markup. CSS bracket balance unchanged. Log entry per Rule 2.5. Set remains 2.1-2.32. */

/* ============================================================
   REVISION LOG — BATCH 186 (FG Opus Claude)
   No code change. Batch 186 full-codebase audit; fixes landed in app.js,
   index.html, get-ip-index.ts, supabase-security-migration.sql. The Re-Open
   modal and History section (Batch 185 R5) reuse existing classes — all
   confirmed present, none added/removed. Rule set remains 2.1-2.32.
   ============================================================ */

/* ============================================================
   BATCH 187 — styles.css (R2, R3, R5).
   R5 — .toast-center + @keyframes centerToastIn (center-screen "Duplicates Cleared"
     toast, distinct from the corner .toast).
   R3 — .trending-refresh-btn (+ :hover, + .trending-refreshing fill/spin) and
     @keyframes trendingSpin.
   R2 — .load-new-discover-btn (+ :hover, + .discover-loading-new spin), reusing
     trendingSpin. No existing rule modified or removed. Rule set remains 2.1-2.32.
   ============================================================ */

/* ============================================================
   REVISION LOG — BATCH 188 (FG, audit-driven fixes)
   ============================================================
   No style change. Batch 188 fixed app.js audit findings F1/F2/F4 (logic only) and
   added an F2 retroactive detection diagnostic to supabase-schema.sql. No CSS rule
   added, modified, or removed. Rule set remains 2.1-2.32.
   ============================================================ */

/* ============================================================
   REVISION LOG — BATCH 189 (FG, Autarch-directed UI/integrity fixes)
   ============================================================
   Batch 189 (R1 verdict/judgement relabel; R2 badge counts = exact distinct holders;
   R3 Badge Distribution active-tab persistence; R4 AC forced-expand page-wipe
   resilience) is confined to app.js. R5 is an Autarch infra action (gallery_posts
   57014 timeout). No CSS rule added, modified, or removed. Rule set remains 2.1-2.32.
   ============================================================ */

/* ============================================================
   PENAL GUIDE ICON — Batch 191 (Autarch-directed)
   Paperwork/clipboard icon beside a violation code on the report
   investigation modal and the dossier violation card. Glows blue until
   its guide is first opened this session (.penal-guide-glow); after
   opening it stays clickable without the glow.
   ============================================================ */
.penal-guide-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 26px;
  height: 26px;
  padding: 0;
  border: 1px solid var(--border);
  border-radius: var(--radius, 6px);
  background: var(--ash-mid);
  color: var(--sepia-light);
  cursor: pointer;
  vertical-align: middle;
  transition: background 0.15s, color 0.15s, box-shadow 0.2s, border-color 0.2s;
}
.penal-guide-icon:hover {
  color: var(--parchment);
  border-color: var(--border-strong);
  background: #46403600;
  background: var(--ash-mid);
}
.penal-guide-icon.penal-guide-glow {
  color: #7fb6ff;
  border-color: rgba(90, 150, 255, 0.6);
  box-shadow: 0 0 6px rgba(90, 150, 255, 0.55), 0 0 12px rgba(90, 150, 255, 0.35);
  animation: penalGuidePulse 1.8s ease-in-out infinite;
}
@keyframes penalGuidePulse {
  0%, 100% { box-shadow: 0 0 5px rgba(90, 150, 255, 0.45), 0 0 10px rgba(90, 150, 255, 0.25); }
  50%      { box-shadow: 0 0 9px rgba(90, 150, 255, 0.75), 0 0 18px rgba(90, 150, 255, 0.5); }
}

/* ============================================================
   SANCTION BUTTON — Batch 191 (Action Report)
   Green = not sanctioned (clickable to open the duration dropdown).
   Red   = active sanction (inert; duration auto-lifts it).
   ============================================================ */
.sanction-btn {
  font-family: 'Cinzel', serif;
  font-size: 0.72rem;
  letter-spacing: 0.06em;
  padding: 0.4rem 0.9rem;
  border-radius: var(--radius, 6px);
  cursor: pointer;
  transition: box-shadow 0.2s, background 0.15s, color 0.15s;
}
.sanction-btn.sanction-green {
  background: rgba(46, 107, 58, 0.18);
  color: #7ec98a;
  border: 1px solid rgba(90, 200, 120, 0.5);
  box-shadow: 0 0 6px rgba(70, 200, 110, 0.45);
}
.sanction-btn.sanction-green:hover {
  box-shadow: 0 0 10px rgba(70, 200, 110, 0.7);
}
.sanction-btn.sanction-red {
  background: rgba(224, 96, 96, 0.18);
  color: #e89090;
  border: 1px solid rgba(224, 96, 96, 0.6);
  box-shadow: 0 0 6px rgba(224, 96, 96, 0.5);
  cursor: default;
}

/* ============================================================
   REVISION LOG — BATCH 190 / 191 (FG, Autarch-directed)
   ------------------------------------------------------------
   Batch 190: no styles.css change (app.js-only audit fixes).
   Batch 191 (CSS CHANGE in this file):
     - .penal-guide-icon + .penal-guide-glow (paperwork icon beside a violation
       code; pulsing blue glow until its guide is first opened this session).
     - .sanction-btn / .sanction-green / .sanction-red (Action Report sanction
       control: green = not sanctioned, red = active sanction).
   No existing rule modified or removed. Rule set 2.1-2.32.
   Batch 192: no styles.css change. Findings A (SQL: user_sanctions table + RLS) and
   C (app.js report-cap parameter fix) add no styling. The existing .sanction-* rules
   above now have a working backing table. Rule set 2.1-2.32.
   Batch 193 (R2, R5 styling): R2 — .load-new-discover-btn.discover-loading-new now
   glows gold (box-shadow) in addition to the spin; this loading state is shared by the
   Discover and Community load-more icons. R5 — added .violation-check-row / .violation-check
   / .violation-check-box (empty outline → fills solid white when ticked) and the
   .violation-check-bitch red-tinted variant for the standalone B.I.T.C.H ban override.
   No existing rule removed. Rule set 2.1-2.32.
   ============================================================ */

/* ============================================================
   REVISION LOG — BATCH 194 (FG audit-fix round)
   ============================================================
   No styles.css change this batch. The audit-fix round touched app.js (Findings 1, 2,
   4, 5), supabase-schema.sql + supabase-auth-rls.sql (Findings 2, 3), and index.html
   (Finding 6 — documented the codeOnlyOverlay/session-warning-* class carryover; the
   session-warning-* rules here are retained intentionally, used only by that one card).
   Rule set 2.1-2.32.
   ============================================================ */

/* ============================================================
   REVISION LOG — BATCH 195 (FG feature/defect round, revs 1–11)
   ============================================================
   styles.css carries REV 2 and REV 10.
   REV 2 — .user-lib-badge.report-line: the User Library report badge drops to its own line
     below the strike badge and reads as one pill ("1 Report" / "2 Reports"); inline-block,
     margin-left:0, small margin-top. Previously the orange report badge crowded the yellow
     strike badge inline, wrapping the label awkwardly.
   REV 10 — .toast-verdict (+ .guilty / .acquit modifiers): centered verdict-confirmation toast
     at z-index 10002 so it sits ABOVE the dossier/report modals (10000) and overlays (10001);
     the old corner .toast (9999) was hidden behind the open window. Guilty tints crimson,
     acquit tints green, neutral stays sepia. Non-interactive.
   Rule set 2.1-2.32.
   ============================================================ */

/* ============================================================
   REVISION LOG — BATCH 196 (FG audit-fix round; Autarch-directed)
   ============================================================
   No styles.css change this batch. The round is a single security-hardening fix in app.js (S1):
   the WSM menu items escape post.id and the topic key via the new _jsq() helper before
   interpolating into the inline reportWSM onclick. No selectors, classes, or rules added or
   removed. Rule set 2.1-2.32.
   ============================================================ */

/* ============================================================
   REVISION LOG — BATCH 197 (FG; Autarch-directed "Proceed with the revisions")
   R3 — Added .brand-tab-link (About/App header tabs beside the wordmark) and .about-body
   (About-section prose layout). R5 — .discovery-trending-label given matching top border-radii
   so scrolling posts no longer bleed through the seam at the top of the Trending bar; no other
   selectors/rules added or removed. All other R-items were app.js/index.html. Rule set 2.1-2.32.
   ============================================================ */

/* ============================================================
   BATCH 198 — AUDIT-FIX ROUND (no code change to this file)
   ============================================================
   Three audit findings fixed, all in app.js: (1) SEC-1 — systemic _jsq() escaping of
   every inline-handler JS-string interpolation (549 wrapped, 0 raw remaining; latent,
   not exploitable in current data; render-time, retroactive); (2) dead-code removal of
   flagAsAI(); (3) B197 convention gap folded into the SEC-1 pass. No SQL this batch.
   No new numbered rule (set remains 2.1-2.32). Outstanding code defects: NONE.
   ============================================================ */

/* ============================================================
   REVISION LOG — BATCH 209 (FG audit-fix round; Autarch-directed "Proceed with the fixes")
   ============================================================
   No styles.css change this batch. Batch 209 fixed one verified Outstanding code defect in app.js
   (audit Finding 1): the Struck Users — Daily Record drilldown bucketed strikes by raw UTC date while
   Batch 208's _contiguousDateKeys walk emits Arizona-local (_azDateKey) day keys, so strikes
   timestamped 00:00-06:59 UTC rendered "0 users struck" on their real Phoenix day. Fixed by keying
   _struckUsersByDate and the struck metric-card today/yesterday lookups on _azDateKey (one shared key
   space); the posts/members UTC deltas were preserved; retroactive by construction. The audit's other
   surfaced items were correctly not actioned (2.14). Logged per 2.5. No SQL. Outstanding defects: NONE.
   ============================================================
   ============================================================
   REVISION LOG — BATCH 208 (FG; Autarch-directed "Proceed with the revisions")
   ============================================================
   Two CSS defects fixed this file (Items 1-2 of five; Items 3-5 were app.js).
   Item 1 — Sticky comment-bar bottom bleed. A sticky bottom:0 bar pins at its scroll
   container's CONTENT-box edge; when that container has bottom padding the bar sits above the
   painted bottom and content scrolling in the strip bleeds through beneath the bar. The shipped
   padding-bottom:2.5rem only pads the bar's content UP (proven insufficient — it was already
   present and the bleed persisted). Added margin-bottom:-2.5rem to .sticky-modal-bar: drops the
   painted box so its opaque --ash fill covers the strip; padding+margin cancel in layout
   (footprint unchanged). Seals notif-detail window, thread modal, gallery overlay, sidebar viewers
   in one rule. Scroll-axis overflow on overflow-y:auto parents is scrollable, not clipped.
   Item 2 — Trending section-label top bleed. The label stuck to top:0 (the wrapper content-box
   top, inside its 1px border), leaving the border band above where a scrolling card flashed
   through before pinning; the prior margin-top:-1px was sub-pixel only. Set top:-1px (anchor at the
   border edge) + margin-top:-2px (bleed the fill up over the band, scroll axis, not clipped).
   Removed the now-stale top:0. CSS braces 572/572. No new rule. Outstanding code defects: NONE.
   ============================================================ */

/* ============================================================
   REVISION LOG — BATCH 207 (FG audit-fix round; Autarch "Proceed with the fixes")
   ============================================================
   No styles.css change this batch. Batch 207 fixed seven findings in app.js: the residual overlay
   reply-chain auto-open defect (Items 1-2), removed the dead mc-row- selector (Item 4), three
   openProfile _jsq escaping sites (Items 5-7), and aligned the overlay comment sort to the canonical
   oldest-first order (Item 3, Autarch-reversible). Item 2 (feed-header centering, B205) REMAINS
   deferred pending the Autarch's A/B ruling — not touched. Logged here per 2.5. No SQL.
   Outstanding code defects: NONE.
   ============================================================ */

/* ============================================================
   REVISION LOG — BATCH 206 (FG audit-fix round; Autarch "Proceed with the fixes")
   ============================================================
   No styles.css change this batch. Batch 206 completed the Batch 205 overlay-scoping guard
   across all id-colliding gallery comment controls and fixed the delete re-render/orphan and
   reply count-badge defects (app.js, Items 1-6). Logged here per 2.5. No SQL. Item 2
   (feed-header centering, the styles.css item) REMAINS deferred pending the Autarch's A/B
   ruling — not touched. Outstanding code defects: NONE.
   ============================================================ */

/* ============================================================
   REVISION LOG — BATCH 205 (FG revision round; Autarch "Proceed with the revisions")
   ============================================================
   No styles.css change this batch. Batch 205 threaded the gallery post overlay's comments
   + added a working Reply path (app.js, Item 3), and folded the live gallery composite index
   into supabase-schema.sql (Item 1). Item 2 (feed-header centering, the styles.css item)
   is DEFERRED pending the Autarch's A/B ruling and was deliberately NOT applied (2.1 — no
   unauthorized aesthetic pick). Logged here per 2.5. No SQL produced this batch.
   No new numbered rule (set remains 2.1-2.32).
   ============================================================ */

/* ============================================================
   REVISION LOG — BATCH 204 (FG audit-fix round; Autarch "Proceed with the fixes")
   ============================================================
   No styles.css change this batch. Batch 204 fixed six code defects from the Batch 203
   audit across app.js (F2, F3, F7, F8) and db.js (F1, F9). Logged here per 2.5.
   No SQL this batch. No new numbered rule (set remains 2.1-2.32). Outstanding code defects: NONE.
   ============================================================ */

/* ============================================================
   REVISION LOG — BATCH 203 (FG revision round; Autarch "Proceed with the revisions")
   ============================================================
   styles.css change this batch:
   R4 — .feed-header changed from a horizontal flex row (justify-content:space-between)
        to a centered vertical column (flex-direction:column; align-items:center;
        text-align:center), so the "Community Feed" label sits centered atop the main
        feed column with the filter chips centered directly beneath it. .feed-filters
        gained justify-content:center; .section-title bottom margin zeroed within
        .feed-header only (no change to the global .section-title rule). Layout-only;
        the Load New Posts control (already centered) and the post feed are untouched.
   No SQL this batch. No new numbered rule (set remains 2.1-2.32). Outstanding code defects: NONE.
   ============================================================ */

/* ============================================================
   REVISION LOG — BATCH 199 (FG feature/defect round; Autarch "Proceed with the revisions")
   ============================================================
   styles.css changes this batch:
   R1 — .brand-home-link { cursor:pointer } + hover color on .brand-name (wordmark reads
        as a clickable tab to the landing page).
   R4 — .about-body p { text-indent: 2em } (first-line indent on every About paragraph).
   R5 — .about-body p.about-signature (right-aligned italic Cinzel sepia, no indent) for
        the "—The Autarch." line.
   No SQL this batch. No new numbered rule (2.1-2.32). Outstanding code defects: NONE.
   ============================================================
   REVISION LOG — BATCH 202 (FG audit-fix round; Autarch "Proceed with the fixes")
   ============================================================
   styles.css change this batch:
   R1 — .discovery-trending-label corner-shoulder seal corrected. The B201 side-bleed
        (margin-left/right -1px + side padding + box-sizing) was UNREACHABLE: the wrapper is
        overflow-y:auto, which computes overflow-x to auto as well, so the -1px side margins were
        clipped by the scroll container and never reached the corner arcs. The reliable seal needs
        no side bleed — the wrapper's own overflow clip rounds every scrolling card to its shape
        (cards cannot paint into the corner arcs) and this label's matched top radii + opaque
        --ash-mid fill + z-index:6 cap the top band. Kept margin-top:-1px (scroll-axis seam closure,
        not clipped sideways); REMOVED the side margins, side padding, and box-sizing.
   (Other findings A/B/C/6 are app.js; R6 z-index correction is inline in index.html.)
   No SQL this batch. No new numbered rule (2.1-2.32). Outstanding code defects: NONE.
   ============================================================
   REVISION LOG — BATCH 201 (FG feature/defect round; Autarch "Proceed with the revisions")
   ============================================================
   styles.css changes this batch:
   R1 — .discovery-trending-label: sealed the Trending sticky-bar bleed. B197 R5 covered only
        the flat top edge; content still bled through the wrapper's rounded-corner shoulders and
        the seam above the sticky as cards scrolled under it (--ash-mid was already opaque — the
        gap was geometric). Now bleeds the opaque label to the wrapper's inner edges (margin-top
        -1px to close the seam; margin-left/right -1px + matching side padding to cover the 8px
        corner arcs; box-sizing:border-box so the text does not shift) and raised z-index 2 -> 6
        above any scrolling card.
   (R6 — the Discover search bar sticky treatment was applied inline in index.html
        #discoverySearchBar: position:sticky; top:72px; opaque --ash bg; z-index above the gallery.
        Noted here for cross-reference; no rule added to styles.css for it.)
   No SQL this batch. No new numbered rule (2.1-2.32). Outstanding code defects: NONE.
   ============================================================
   REVISION LOG — BATCH 200 (FG audit-fix round; Autarch "Proceed with the fixes")
   ============================================================
   styles.css change this batch:
   Finding 4 — .brand-home-link:hover .brand-name color changed --sepia -> --sepia-light.
        The brand name rests at --sepia (see .brand-name), so the B199 hover-to-sepia was a
        no-op; --sepia-light is a visibly brighter on-theme highlight giving real hover
        feedback. (The other seven Batch-200 findings are in app.js.)
   No SQL this batch. No new numbered rule (2.1-2.32). Outstanding code defects: NONE.
   ============================================================
   ============================================================
   REVISION LOG — BATCH 217 (Autarch: "Proceed with the revisions")

   No styles.css change this batch. Batch 217 (registration redesign) is confined
   to app.js (token-picker removal, typed-DOB parsing, the confirmation-gated flow
   + post-confirmation overlay), index.html (picker removal, DOB input type), db.js
   (detectSessionInUrl), and a new Edge Function (send-access-code-index.ts). The
   post-confirmation overlay (regFlowModal) reuses existing .modal-overlay/.modal-box
   classes — no new CSS needed. Set remains 2.1-2.32. Outstanding code defects: NONE.
   REVISION LOG — BATCH 216 (Autarch: "No, just do it. Seamless implementation; no hasty work.")

   No styles.css change this batch. Batch 216 is Stripe Phase 2 (the front-end): the
   Stripe Modal + all charge-surface rewiring live in app.js; the signupStep2 raw card
   fields are removed in index.html; the users_public rebuild is in the security SQL.
   NOTE FOR FUTURE PALETTE WORK: the Stripe Payment Element renders inside Stripe's
   iframe where CSS variables are unreachable, so app.js _stripeAppearance() MIRRORS
   the :root palette values (#c9a96e sepia, #2a2620 ash, #1e1b17 ash-dark, #f5eedf
   parchment, #c0392b crimson-light, #7a6e5f text-muted, #5a5248 ash-light, 4px
   radius, the rgba(201,169,110,...) borders) — if :root changes, that map must move
   in lockstep. Set remains 2.1-2.32. Outstanding code defects: NONE.
   REVISION LOG — BATCH 215 (Autarch rulings: grace period + cards-on-file; Phase 1)

   No styles.css change this batch. Batch 215 is the Stripe Phase 1 backend: five new
   Edge Functions + SQL folded to source-of-truth (guarded users stripe columns,
   service-role-only billing RPC, stripe_payment_intent_id provenance columns).
   Front-end (Phase 2) not started. Set remains 2.1-2.32.
   REVISION LOG — BATCH 214 (Autarch "Proceed with the revisions")

   No styles.css change this batch. Batch 214 (fee-on-top processing-fee disclosure)
   is inline-styled markup in app.js (tip-menu fee labels + footer, patron-tier fee
   lines, past-volumes fee line, purchase-button copy) and index.html (membership
   $5.46/mo charged copy, reusing the existing .form-note class). No new selectors.
   No SQL. Set remains 2.1–2.32. Outstanding code defects: NONE.
   ============================================================
   REVISION LOG — BATCH 213 (Autarch "Proceed with the revisions")

   No styles.css change this batch. Batch 213 R2 reuses the existing .sticky-modal-bar
   class (Batch-208 opacity/edge-seal) unchanged — the fix is in app.js, removing the
   nested 250px inner-scroll wrapper in the non-forum _buildCommentSection branch so the
   sticky bar anchors to the modal scrollport. The class rule was NOT modified. R1a is a
   SQL admin-gate; R1b (update_user_badge_rpc forgery hardening) shipped in the same SQL
   file. About-page copy edits in index.html (no styles.css change). Brace balance unchanged
   574/574. Set
   remains 2.1–2.32.

   REVISION LOG — BATCH 212 (Autarch "Proceed with the revisions")
   ============================================================
   No styles.css change this batch — Batch 212 is a render-layer fix in app.js only: the pop-up
   comment composer is now the fixed bottom row of its flex column across all four forum kinds
   (debate, story, discussion, open_house) and the notif-detail/Announcement comment thread
   (min-height:0 on the scroll box so it scrolls internally; bar position:sticky -> flex-shrink:0).
   The .sticky-modal-bar class rule was NOT modified. Brace balance unchanged 574/574.
   No SQL this batch. No new numbered rule (set remains 2.1-2.32). Outstanding code defects: NONE.
   ============================================================
   REVISION LOG — BATCH 211 (Autarch "Proceed with the fixes")
   ============================================================
   styles.css change this batch:
   Finding 1 (dead code) — the .author-controls rule (===== AUTHOR CONTROLS ===== section)
        was retired to a provenance comment. Its only two consumers (#authorControls,
        #loreKeeperControls) were empty permanently-hidden divs, removed from index.html this
        batch along with their inert updateUI ac/lc toggles in app.js. No element carries the
        class any longer. Brace balance re-counted 574/574 (the commented rule's braces remain a
        matched pair). No live style affected — the selector matched nothing visible.
   Findings 2 (metra teardown) and 3 (convention note) are SQL-side; no styles.css change.
   No new numbered rule (set remains 2.1-2.32). Outstanding code defects: NONE.
   ============================================================
   REVISION LOG — BATCH 210 (Autarch "Proceed with the revisions")
   ============================================================
   styles.css change this batch:
   Item 2 — .feed-main { min-width: 0 } added (after .feed-layout). The 1fr feed-main
        grid track inherited implicit min-width:auto, so wide children (YouTube embeds,
        full-bleed .post-card-image) forced the track wider than its share; the centered
        .feed-header then centered across the over-wide track and landed left of the posts.
        min-width:0 lets the track hold its computed width so the header centers over the
        actual feed column, aligned with the posts beneath. Layout-only; retroactive (2.21)
        — applies to every feed render. (Batch 210 Item 1 is an app.js-only markup fix to the
        gallery overlay comment bar; no styles.css change for it — the .sticky-modal-bar class
        seal was already correct and is untouched.)
   No SQL this batch. No new numbered rule (2.1-2.32). Outstanding code defects: NONE.
   ============================================================ */

/* ============================================================
   REVISION LOG — BATCH 217 AUDIT-FIX PASS (Autarch: "Proceed with the fixes")
   ============================================================
   No styles.css change this pass — the Batch 217 audit fixes are confined to
   app.js (boot-path flash, orphaned-Auth-user wedge, duplicate-email handling,
   dead-code removal, honest email-status node, modal-path toast). The new
   #regFlowEmailStatus node reuses inline styles; no CSS added. Set remains
   2.1-2.32. Outstanding code defects: NONE.
   ============================================================ */

/* ============================================================
   REVISION LOG — BATCH 217 ROUND-TRIP FIX (Autarch: live test)
   ============================================================
   No styles.css change this pass — confirmation round-trip fix is app.js only
   (cross-tab localStorage stash, session-parse retry, orphan-stash guard).
   Set remains 2.1-2.32. Outstanding code defects: NONE.
   ============================================================ */

/* ============================================================
   REVISION LOG — BATCH 218 (Autarch: "Proceed with the revisions" — registration
   redesign: email confirmation retired)
   ============================================================
   THREE additions (placed with the toast family after .toast-verdict):
   - .toast-center.error — crimson tint for signup validation rejections, which
     now land in the middle of the page per Autarch directive (app.js
     _centerToast gained the optional 'error' type).
   - .toast-sticky (+ .toast-sticky-x) — PERSISTENT center toast: no auto-
     dismiss, closed only by its X; z-index 10003 (above verdict 10002 and all
     modals/overlays). Carries the post-payment access-code instruction.
   - .gold-spinner + @keyframes dmsGoldSpin — the spinning circle of gold
     (border-top var(--gold), 0.9s linear) shown while the membership payment is
     confirmed and the access-code email dispatches.
   No new numbered rule (set remains 2.1-2.32). Outstanding code defects: NONE.
   ============================================================ */

/* ============================================================
   REVISION LOG — BATCH 219 (Autarch: "Proceed with the fixes" — live proving
   run Layer 5: Stripe API-version drift + source-of-truth reconciliation)
   ============================================================
   No styles.css change this batch. Fixes confined to
   create-subscription-index.ts (Stripe-Version pinned 2025-03-31.basil; expand
   moved to latest_invoice.confirmation_secret with legacy fallback;
   stripe_customer_id persisted immediately on creation — redeploy is an
   Autarch action), supabase-security-migration.sql (Layer-2 users.password
   ALTER folded in per 2.31), and supabase-schema.sql (password nullable at
   creation, retroactive rebuild fix). Other Stripe functions swept and
   confirmed Basil-stable. sovereign_log 42501: divergence theory later
   DISPROVEN (live policies match canon; true cause = db.js
   return=representation read-back, fixed Batch 220). Logged here per 2.5.
   No new numbered rule (set remains 2.1-2.32). Outstanding code defects: NONE.
   ============================================================ */

/* ============================================================
   REVISION LOG — BATCH 220 (Autarch: "Proceed with the fixes" — Layer 6 CSP
   + sovereign_log read-back + narrative corrections)
   ============================================================
   No styles.css code change this batch (one Batch 219 ledger line corrected —
   the sovereign_log divergence theory, disproven by the Autarch's pg_policies
   diagnostic). Fixes confined to index.html (CSP amended for Stripe:
   script-src, frame-src, connect-src) and db.js (writeSovereignLog →
   return=minimal; the read-back aborted non-admin inserts with 42501).
   Logged here per 2.5. No new numbered rule (set remains 2.1-2.32).
   Outstanding code defects: NONE.
   ============================================================ */

/* ============================================================
   REVISION LOG — BATCH 221 (Autarch: "Proceed with the fixes" — audit-fix
   round on the session self-audit findings)
   ============================================================
   No styles.css change this batch. F2 — index.html frame-src gains
   https://m.stripe.network (completing Stripe's documented CSP set).
   F3 — create-subscription-index.ts no-op p_customer_id removed from the
   second RPC call (redeploy is an Autarch action). F1 — legacy
   payment_intent fallback retained by audit recommendation.
   Logged here per 2.5. No new numbered rule (set remains 2.1-2.32).
   Outstanding code defects: NONE.
   ============================================================ */

/* ============================================================
   REVISION LOG — BATCH 222 (Autarch: "proceed with the fixes" — the §5a
   autocomplete/keychain defect, traced to root then executed)
   ============================================================
   No styles.css change this batch. Safari treated the page as one
   ambiguous login context (no autocomplete hints on password inputs;
   "off" ignored on credential fields), anchoring its keychain popup to
   whatever held focus — including Stripe's iframe card field. Fix landed
   in index.html (both login pairs -> "username"/"current-password", login
   autofill KEPT; four registration password inputs -> "new-password") and
   app.js (regflow modal's two password inputs -> "new-password").
   Access-code fields untouched. Hints, not commands — residue is
   Safari's. Logged here per 2.5. No new numbered rule (set remains
   2.1-2.32). Outstanding code defects: NONE.
   ============================================================ */

/* ============================================================
   REVISION LOG — BATCH 223 (Autarch: "Proceed with the revisions")
   ============================================================
   CHANGED IN THIS FILE: (R2/R3) Trouble Shoot panel styling added after the
   about-signature rule — .ts-body/.ts-option/.ts-option-head/.ts-chev/.ts-panel/
   .ts-disclaimer/.ts-optional/.ts-submit. .ts-submit rides the existing
   .btn-danger crimson for the red submit button. Braces balanced 591/591.
   No new numbered rule (set remains 2.1-2.32).
   Outstanding code defects: 1 (Finding 2 — _cancelMembership writes nothing).
   ============================================================ */

/* ============================================================
   REVISION LOG — BATCH 225 (Autarch: "Proceed with the revisions")
   ============================================================
   CHANGED IN THIS FILE: cancellation-lifecycle styling appended before the
   RESPONSIVE block — (R2) .ts-context landing textarea; (R10) .cancel-refresh-btn
   + .cancel-refresh-icon (discover-feed gold spin via .spinning, reusing
   @keyframes dmsGoldSpin); (R3) .cancel-confirm-btn (+ .sent green state),
   .cancel-action-btn (stacked two-line Cancel Account / Delete Request, gold-on-
   hover), .cancel-context-box; (R4) .cancel-lea checkbox label; (R9) .cleared-search
   bar. Gray-gating on the action buttons is applied inline (opacity + pointer-
   events) from the render, lifted by reply_received OR lea. Braces balanced
   607/607. No new numbered rule (set remains 2.1-2.32).
   Outstanding code defects: NONE.
   ============================================================ */

/* ============================================================
   REVISION LOG — BATCH 225 (audit-fix: Finding A)
   ============================================================
   CHANGED IN THIS FILE: .cancel-reply-btn (+ :hover, + .set green latch) added
   beside .cancel-confirm-btn for the new "Mark Reply" / "Reply Received" control
   that closes Finding A. Braces balanced 610/610. No new numbered rule
   (set remains 2.1-2.32).
   Outstanding code defects: NONE.
   ============================================================ */

/* ============================================================
   REVISION LOG — BATCH 226 (Autarch: "Proceed with the fixes")
   ============================================================
   NO CODE CHANGE IN THIS FILE. The cancel-* and .cancel-refresh-btn.spinning
   rules were already correct; Finding 5 was a JS-timing defect (the spinning
   node was destroyed before paint), fixed in app.js, not here. Findings 1, 3,
   4, R1 landed in the Edge Functions, db.js, and app.js. Finding 2 struck.
   Logged per 2.5. No new numbered rule (set remains 2.1-2.32).
   Outstanding code defects: NONE.
   ============================================================ */

/* ============================================================
   REVISION LOG — BATCH 227 (Autarch: "Proceed with the revisions")
   ============================================================
   NO CODE CHANGE IN THIS FILE. Batch 227 delivered R1 (profile blank-page fix),
   R2 (Terms of Agreement clickwrap gate), R3 (Stripe billing portal), R4 (LEA
   auto-refund), R5 (full-footprint removal of cancelled/banned users). Changes
   live in app.js, db.js, index.html, cancel-account-index.ts, the new
   billing-portal-index.ts, and supabase-security-migration.sql. Logged per 2.5.
   OUTSTANDING CODE DEFECTS: NONE.
   ============================================================ */

/* ============================================================
   REVISION LOG — BATCH 227 AUDIT-FIX (Autarch: "Proceed with the fixes")
   ============================================================
   NO CODE CHANGE IN THIS FILE. Batch 227 self-audit fixes: Finding A/E (removed
   dead saved_posts/followed_forums scrub from purge_user_footprint), Finding B
   (replaced auth.role() gate with p_admin_assert param), Finding C (_toaAccepted
   reset), Finding D (dead toast arg). Changes in supabase-security-migration.sql,
   cancel-account-index.ts, app.js, db.js. OUTSTANDING CODE DEFECTS: NONE.
   ============================================================ */

/* ============================================================
   REVISION LOG — BATCH 228 (Autarch: "Proceed")
   ============================================================
   CHANGED IN THIS FILE (styles.css):
   R1 — Activity-feed sticky-header sliver. .profile-feed-header-sticky painted its
   opaque background:var(--ink) only through padding-bottom:0.6rem, then carried a
   transparent margin-bottom:0.75rem. Margins do not inherit the background, so a
   post image scrolling up under the sticky tab bar bled through that 0.75rem band
   — the visible sliver. Folded the margin into padding (0.6+0.75=1.35rem) and set
   margin-bottom:0, so the opaque band now covers the full seam down to the content.
   Identical total spacing; the gap is sealed. Same class as the Batch 197/198
   sticky-bar bleed. Render-time/CSS fix, retroactive on next render (2.21).
   (R2 is the liked-by window video thumbnail — see app.js.)
   Brace-balanced. DEPLOY: front-end. OUTSTANDING CODE DEFECTS: NONE.
   ============================================================ */

/* ============================================================
   REVISION LOG — BATCH 229 (Autarch: "Proceed with the fixes" — audit findings)
   ============================================================
   CHANGED IN THIS FILE (styles.css):
   F2 — Same sticky-bleed class as Batch 228 R1, found in the audit on .forum-kind-tabs.
   It paints opaque background:var(--ink) with padding-bottom:0 and a transparent
   margin-bottom:1.5rem, so a card scrolling under the sticky forum-tab bar could
   show through that band. Unlike R1, this element's border-bottom sits tight to the
   tabs and already visually masks the bleed; folding the margin into padding would
   push that border 1.5rem off the tabs (a design change). Hardened instead with
   box-shadow: 0 1.5rem 0 var(--ink) to paint the margin band opaque, leaving the
   border position and total 1.5rem spacing unchanged. Render-time/CSS fix.
   (F1 dead-branch removal and F3 thumbnail-nav unify are in app.js.)
   Brace-balanced (610/610). DEPLOY: front-end. OUTSTANDING CODE DEFECTS: NONE.
   ============================================================ */

/* ============================================================
   REVISION LOG — BATCH 230 (Autarch: "Proceed" — spelling correction)
   ============================================================
   NO CODE CHANGE IN THIS FILE. Batch 230 is a display-text spelling fix only:
   "Trouble Shoot" → "Troubleshoot" in two user-facing labels in index.html
   (brand-tab nav button + #troubleshootSection <h2>). No selector, class, or
   rule changed; the .ts-* and #troubleshootSection styling is untouched.
   Logged per 2.5. Set remains 2.1-2.32. OUTSTANDING CODE DEFECTS: NONE.
   ============================================================ */

/* ============================================================
   REVISION LOG — BATCH 231 (Autarch: "Proceed with the revisions")
   ============================================================
   NO CODE CHANGE IN THIS FILE. Batch 231 is a front-end JS flow alignment in
   app.js only. No selector, class, or rule changed. Logged per 2.5. Set
   remains 2.1-2.32.
   OUTSTANDING CODE DEFECTS: over-broad USING(true) UPDATE on posts/comments/
   gallery_posts (Phase B), not built this session per Autarch scope.
   ============================================================ */

/* ============================================================
   REVISION LOG — BATCH 232 (Phase B — over-broad UPDATE lockdown)
   ============================================================
   NO CODE CHANGE IN THIS FILE. db.js write-wrappers repointed to SECURITY DEFINER
   RPCs; posts/comments/gallery_posts UPDATE locked to author-or-admin.
   Set remains 2.1-2.32. OUTSTANDING CODE DEFECTS: NONE.
   ============================================================ */

/* ============================================================
   REVISION LOG — BATCH 233 (Autarch directive: payment-gated account creation)
   ============================================================
   NO CODE CHANGE IN THIS FILE. Batch 233 reorders registration so the account is
   created only when membership payment confirms, and tears down the orphan auth
   user on payment failure/abandon. All changes are in app.js + db.js and the
   delete-pending-account / create-subscription / stripe-webhook Edge Functions.
   No selector, rule, or token changed here.
   Set remains 2.1-2.32. OUTSTANDING CODE DEFECTS: NONE.
   ============================================================ */

/* ============================================================
   REVISION LOG — BATCH 234 (Autarch: "Proceed with the fixes" — audit fixes)
   ============================================================
   NO CODE CHANGE IN THIS FILE. Batch 234 fixes audit findings in the payment-
   gated registration flow (app.js + the delete-pending-account / stripe-webhook
   Edge Functions). No selector, rule, or token changed here.
   Set remains 2.1-2.32. OUTSTANDING CODE DEFECTS: NONE.
   ============================================================ */

/* ============================================================
   REVISION LOG — BATCH 235 (Autarch-directed — payment-first guard fix)
   ============================================================
   NO CODE CHANGE IN THIS FILE. Batch 235 fixes the create-subscription Edge
   Function only: its pre-Batch-233 row-required guard (403 "Unknown user")
   contradicted the payment-first order, so every fresh signup failed before
   Stripe was reached. No selector, rule, or token changed here.
   Set remains 2.1-2.32. OUTSTANDING CODE DEFECTS: NONE.
   ============================================================ */

/* ============================================================
   REVISION LOG — BATCH 236 (Autarch go-live — Stripe publishable key swap)
   ============================================================
   NO CODE CHANGE IN THIS FILE. Batch 236 swaps app.js's STRIPE_PUBLISHABLE_KEY
   from the test key to the live publishable key (same account as the live secret
   key), fixing the Stripe Elements-session 400 that appeared after Batch 235.
   No selector, rule, or token changed here.
   Set remains 2.1-2.32. OUTSTANDING CODE DEFECTS: NONE.
   ============================================================ */

/* ============================================================
   REVISION LOG — BATCH 237 (Autarch-directed — docx render fidelity, Rev 7)
   ============================================================
   ADDITIVE-ONLY changes to the .dms-doc-render block below, restoring source-document
   structuring that the bare convertToHtml render dropped: first-line paragraph
   text-indent (suppressed on the first para after a heading / first child); centered
   headings (h1/h2/h3 gain text-align:center); and .dms-align-center/right/justify
   classes (plus inline text-align honoring) so a centered/right/justified paragraph in
   the source renders as authored. No pre-existing selector or rule was modified or
   removed, so the working render (paragraphs, bold/italic, headings, lists) cannot
   regress. Paired in app.js with the _docxToHtml alignment styleMap (Rev 7). No
   migration. Set remains 2.1-2.32. Sovereign Isolation intact.
   OUTSTANDING CODE DEFECTS: NONE.
   ============================================================ */
   Styles the converted .docx HTML inside the parchment reader
   page. PDF volumes render to <canvas> and need no rules here.
   ============================================================ */
.dms-doc-render p { margin: 0 0 1rem; }
/* Batch 237 (Rev 7) — Restore source-document structuring that the bare convertToHtml
   render dropped. ADDITIVE ONLY: every rule below is a new selector or property that
   adds structure; no pre-existing rule above/below is altered, so the working render
   (paragraphs, bold/italic, lists) cannot regress.
   1) First-line paragraph indent for prose paragraphs. Suppressed on the first
      paragraph after a heading and on the very first paragraph, matching standard
      typesetting (no orphan indent under a chapter title).
   2) Honor source paragraph alignment. Mammoth (configured with the alignment style
      map at the render sites) emits text-align via inline style OR the
      .dms-align-center/right/justify classes; these rules render them faithfully.
   3) Center headings by default — chapter/section headings in the source are
      typically centered; a left-forced container previously slated them left. A
      source heading carrying explicit alignment still wins via rule (2). */
.dms-doc-render p { text-indent: 1.6em; }
.dms-doc-render h1 + p,
.dms-doc-render h2 + p,
.dms-doc-render h3 + p,
.dms-doc-render > p:first-child { text-indent: 0; }
.dms-doc-render [style*="text-align:center"],
.dms-doc-render [style*="text-align: center"],
.dms-doc-render .dms-align-center { text-align: center; text-indent: 0; }
.dms-doc-render [style*="text-align:right"],
.dms-doc-render [style*="text-align: right"],
.dms-doc-render .dms-align-right { text-align: right; text-indent: 0; }
.dms-doc-render [style*="text-align:justify"],
.dms-doc-render [style*="text-align: justify"],
.dms-doc-render .dms-align-justify { text-align: justify; }
.dms-doc-render h1,
.dms-doc-render h2,
.dms-doc-render h3 {
  font-family: 'Cinzel', serif;
  color: #3a2a18;
  line-height: 1.3;
  margin: 1.4rem 0 0.6rem;
  text-align: center;
}
.dms-doc-render h1 { font-size: 1.5rem; }
.dms-doc-render h2 { font-size: 1.25rem; }
.dms-doc-render h3 { font-size: 1.1rem; }
.dms-doc-render ul,
.dms-doc-render ol { margin: 0 0 1rem 1.4rem; }
.dms-doc-render li { margin: 0 0 0.35rem; }
.dms-doc-render img { max-width: 100%; height: auto; display: block; margin: 1rem auto; }
.dms-doc-render strong { font-weight: 700; }
.dms-doc-render em { font-style: italic; }
.dms-doc-render a { color: #6a4a2a; text-decoration: underline; }

/* ============================================================
   REVISION LOG — BATCH 238 (Autarch-reported — badges not clickable)
   ============================================================
   NO CHANGE IN THIS FILE. Batch 238 is confined to app.js: declared the undeclared
   `privileges` var that threw under 'use strict' and aborted _openBadgeInfo (the live
   cause of non-clickable badges); added an admin description case; and rewrote tierBadge
   so the name-adjacent inline badges render the real badge and are clickable. No SQL,
   no migration (2.31). Set remains 2.1-2.32. OUTSTANDING CODE DEFECTS: NONE.
   ============================================================ */

/* ============================================================
   REVISION LOG — BATCH 239 (Autarch-directed — Discover carries all post types; composer rework)
   ============================================================
   NO CHANGE IN THIS FILE. Batch 239 is confined to app.js: the new-post menu now offers
   Forum/Quote/Image/Video directly (grouped "Discover" item removed; tier-gated Universal
   Post and admin items untouched); the Discover feed now includes category 'forum' posts
   AND reposts (rendered via the shared renderPostCard exactly as they already appear);
   and the Trending bar now ranks forum posts on the same engagement formula as every
   other post (reposts excluded from Trending). No SQL, no migration (2.31).
   Set remains 2.1-2.32. OUTSTANDING CODE DEFECTS: NONE.
   ============================================================ */

/* ============================================================
   REVISION LOG — BATCH 240 (Autarch-directed — follow/unfollow persistence + pending-request durability)
   ============================================================
   NO CHANGE IN THIS FILE. Batch 240 is confined to app.js, db.js, and
   supabase-security-migration.sql: follow/unfollow now persist through new
   SECURITY DEFINER RPCs that write both follower and followee arrays server-side
   (fixes the follower-count desync); the private request→approval path is preserved;
   _isFollowRequestPending hardened. No styles changed. Stale duplicate Batch 239
   ledger block also removed (corrected entry retained).
   Set remains 2.1-2.32. OUTSTANDING CODE DEFECTS: NONE.
   ============================================================ */

/* ============================================================
   REVISION LOG — BATCH 241 (Autarch-directed — badge-earned notifications fix)
   ============================================================
   No styles changed. The fix lives in app.js + db.js + SQL: badge-earned
   notifications now reach every earner (not just the in-session user), revised
   copy "You earned the <Label> Badge"; Saint gain/loss is now instant and
   cross-session on a new persisted users.is_saint column via set_user_saint_rpc.
   Retroactive recovery is subsumed by the persisted markers. The Saint SQL must run.
   Set remains 2.1-2.32. OUTSTANDING CODE DEFECTS: NONE.
   ============================================================ */

/* ============================================================
   BATCH 242 — PARTNER PROGRAM RESTORATION (Autarch-directed)
   Revision-log only — no CSS changed (panel/token/page use inline styles).
   Recorded so all eight log-bearing files carry the batch. OUTSTANDING CODE DEFECTS: NONE.
   ============================================================ */

/* ============================================================
   BATCH 243 — Autarch's Choice persistence + badge/Saint write gating
   Revision-log only — no CSS changed. Recorded so all eight log-bearing
   files carry the batch (2.5). OUTSTANDING CODE DEFECTS: NONE.
   ============================================================ */

/* ============================================================
   BATCH 244 — Partner Outreach Composer (Autarch-directed)
   Revision-log only — no CSS changed. The composer's recipient/subject
   inputs and body textarea reuse the existing .form-group input/textarea
   styling; only functional inline props (rows, resize, white-space:pre-wrap)
   are set on the textarea. Recorded so all eight log-bearing files carry the
   batch (2.5). OUTSTANDING CODE DEFECTS: NONE.
   ============================================================ */

/* ============================================================
   BATCH 245 — Profile data-integrity glitch (Autarch-directed)
   Revision-log only — no CSS changed. The follower/saved fixes (R1/R2/R3)
   are JS-only (count resolution + enforcer gate in app.js) and reuse the
   existing .profile-stat-block, .modal-box, and modal-overlay styling; the
   RLS hardening (R4/R5) is in supabase-schema.sql. Recorded so all eight
   log-bearing files carry the batch (2.5).
   OUTSTANDING CODE DEFECTS: NONE.
   ============================================================ */

/* ============================================================
   BATCH 246 — Live-site defect sweep (Autarch-directed)
   Revision-log only — no CSS changed. The A3 color-scheme fix is a JS
   selector-list correction in app.js (_resetColorScheme now clears the
   same .profile-feed-tab / .profile-feed-tabs / .profile-feed-header-sticky
   the apply path styles); the existing CSS for those classes is unchanged.
   All other batch fixes are JS / Edge Function / SQL. Recorded so all eight
   log-bearing files carry the batch (2.5).
   OUTSTANDING CODE DEFECTS: NONE.
   ============================================================ */

/* ============================================================
   BATCH 247 — How-To accuracy + refresh + username + chapters + saint (Autarch-directed)
   styles.css change (N5): added the .saint-crown class — the winged-crown
   saint badge, em-scaled (height 1em, width 1.25em) so it sits inline beside
   the points-tier badge in the new two-badge tandem, with the same gold
   drop-shadow as the retired wings. The three .saint-wing* classes were REMOVED
   (Batch 247 D3, dead after N5); only the .saint-winged flex wrapper is retained —
   it now hosts the crown + tier icon (the two-badge tandem).
   All other Batch 247 work is JS / HTML / SQL.
   D3 (post-delivery self-audit, same batch): the three .saint-wing* classes were
   REMOVED as dead (no emitter after N5 retired the wings badge); .saint-winged is
   retained as the flex wrapper that now hosts the crown + tier icon.
   OUTSTANDING CODE DEFECTS: NONE.
   ============================================================ */

/* ============================================================
   BATCH 248 — Partners tabs + outreach record + second month free
   (Autarch-directed). Revision-log only — NO CSS changed in this file.
   The tab header, cards, listings, and log feed style inline (matching the
   panel's existing inline-style convention); the ↻ reuses .modal-close with
   an inline right offset. All code lives in app.js, db.js, index.html,
   stripe-webhook-index.ts, and supabase-schema.sql. Recorded so all eight
   log-bearing files carry the batch (2.5).
   OUTSTANDING CODE DEFECTS: NONE.
   ============================================================ */

/* ============================================================
   BATCH 248 — FIX ROUND (Autarch-directed; all changes in app.js: F1 race
   guard, F2 refresh cache clear, H1 email regex, H2 _attrq attribute
   escaping). Revision-log only — no CSS changed. Provenance note recorded in
   the app.js log. This file verified IDENTICAL to the delivered drop.
   OUTSTANDING CODE DEFECTS: NONE.
   ============================================================ */

/* ============================================================
   BATCH 249 — CLOSED-FORUM VISIBILITY SWEEP + NOTIFICATION ENGINE
   REPAIR (all in app.js: Discover/Trending/AC-sidebar deactivated-forum
   filters, ranking self-vote silent-mark fix, FSD + debate-timeout sweeps
   restored to the 60s polling cycle, FM/admin gate added to the timeout
   sweep). Revision-log only — no CSS changed.
   OUTSTANDING CODE DEFECTS: NONE.
   ============================================================ */

/* ============================================================
   BATCH 250 — ALL FSD DURATIONS DOUBLED (Autarch directive; compute
   mapping in app.js, five selector labels in index.html). Revision-log
   only — no CSS changed.
   OUTSTANDING CODE DEFECTS: NONE.
   ============================================================ */

/* ============================================================
   BATCH 251 — PARTNERS PANEL: OUTREACH SEARCH + SORT, STICKY
   CONTROL BAR (Autarch directive). .partners-sticky-bar added (Rule
   2.28: opaque, own compositing layer, top edge sealed by relocating
   #partnersModal .modal-box padding-top onto the title — rest layout
   pixel-identical — bottom margin band painted by --ash box-shadow per
   Batch 229 F2). Search/sort logic in app.js.
   F1 (fix round, audit finding) — mobile title headroom: the relocated
   2rem title padding sat 0.5rem below pre-251 on <=768px (mobile box
   padding is 1.5rem). Matched inside the existing 768px media query;
   equal specificity, later in source — mobile-only; desktop and the
   opacity seals untouched. Retroactive by construction (pure CSS,
   applies on every render).
   OUTSTANDING CODE DEFECTS: NONE.
   ============================================================ */

/* ============================================================
   BATCH 252 — FORUM FEED RE-INTRODUCTION (Autarch directive).
   THIS FILE: .post-tag.feed-bump-badge — the gold "Round N" chip on
   re-introduced debate cards (equal specificity to .post-tag.forum,
   later in source, so gold governs on the combined class). Story bumps
   carry " — Vol. X" in the title (app.js render; no CSS needed). All
   other card styling untouched.
   OUTSTANDING CODE DEFECTS: NONE.
   ============================================================ */

/* ============================================================
   BATCH 253 — CIVILIAN TIER, SECTION 1 (Autarch directive).
   Revision-log only — the fork uses existing form-group/.hidden
   utilities; no new CSS surface.
   OUTSTANDING CODE DEFECTS: NONE.
   ============================================================ */

/* ============================================================
   BATCH 254 — CIVILIAN TIER, SECTION 3 (Autarch directive).
   Revision-log only — no CSS surface.
   OUTSTANDING CODE DEFECTS: NONE.
   ============================================================ */

/* ============================================================
   BATCH 255 — REVISION B S2/S3b/S4/S5 (Autarch directive).
   Revision-log only — guest chrome, panels, and windows style inline
   against existing tokens; no new CSS surface.
   OUTSTANDING CODE DEFECTS: NONE.
   ============================================================ */

/* ============================================================
   BATCH 256 — REVISION B COMPLETION (Autarch directive).
   Revision-log only — no CSS surface.
   OUTSTANDING CODE DEFECTS: NONE.
   ============================================================ */

/* ============================================================
   BATCH 257 — AUDIT FINDINGS F1–F4 FIXED (Autarch directive).
   Revision-log only — no CSS surface.
   ============================================================ */

/* ============================================================
   BATCH 258 — GUEST VIEW FIXES (Autarch rulings). THIS FILE:
   html.guest-mode pierces the Batch 76 not(.has-session) gate for
   exactly the guest surfaces — .feed-layout (grid) and
   #announcementBanner. Nav/forum/shop stay gated for guests.
   OUTSTANDING CODE DEFECTS: NONE.
   ============================================================ */

/* ============================================================
   BATCH 259 — GUEST/MEMBER STATE-TRANSITION FIXES (Autarch-directed).
   THIS FILE: .has-session #enrollTabs {display:none !important} — the
   enrollment tabs are guest-only, hidden by the same gate that hides
   guest chrome on login.
   OUTSTANDING CODE DEFECTS: NONE.
   ============================================================ */

/* ============================================================
   BATCH 260 — AD SYSTEM (Autarch directive: two-audience model).
   RAILS: the empty left/right gutters, all views incl. Guest View.
   Fixed skyscraper containers, OPAQUE per 2.28 (sticky surfaces
   are never see-through), palette-matched so house creatives sit
   in the site's own skin. Populated only for harvestable viewers
   (_renderAdRails gates on _isHarvestable); .hidden otherwise —
   Autocracy gutters stay exactly as they are today, zero layout
   shift in either state. Right rail insets clear of the floating
   action stack column (~64px). Collapse below 1200px: the rails
   never squeeze the content column. z-index 50: above the page
   wash, below every modal/toast layer.
   ============================================================ */
.ad-rail {
  position: fixed;
  top: 90px;
  bottom: 20px;
  width: 170px;
  z-index: 50;
  display: flex;
  flex-direction: column;
  gap: 14px;
  align-items: center;
  overflow: hidden;
}
.ad-rail-left  { left: 14px; }
.ad-rail-right { right: 64px; } /* inset-clears the floating action stack */
.ad-unit {
  width: 160px;
  background: var(--ash-mid, #14100d); /* opaque — 2.28 */
  border: 1px solid var(--border, #2a231c);
  border-radius: var(--radius, 6px);
  overflow: hidden;
  text-decoration: none;
  display: block;
}
.ad-unit img { width: 100%; display: block; }
.ad-unit .ad-unit-tag {
  display: block;
  font-family: Cinzel, serif;
  font-size: 0.55rem;
  letter-spacing: 0.12em;
  color: var(--text-muted, #7a6f60);
  text-align: center;
  padding: 3px 0 4px;
  text-transform: uppercase;
}
@media (max-width: 1200px) {
  .ad-rail { display: none !important; }
}

/* BATCH 279 (restored 278-Adsterra) — ADSTERRA TOP STRIP. A horizontal Sponsored band below the
   nav, above the active feed title; centered, capped to a leaderboard
   width, opaque (2.28). Hidden off-surface and for citizens (.hidden wins
   via display:none!important). min-height reserves space so the mounted
   Native Banner does not shift the feed on arrival. */
.ad-top-strip {
  max-width: 728px;
  margin: 0 auto 1.5rem;
  padding: 0.45rem 0 0.3rem;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.3rem;
  background: var(--ash-mid, #14100d);
  border: 1px solid var(--border, #2a231c);
  border-radius: var(--radius, 6px);
  overflow: hidden;
}
.ad-top-strip .ad-top-strip-tag {
  font-family: Cinzel, serif;
  font-size: 0.55rem;
  letter-spacing: 0.12em;
  color: var(--text-muted, #7a6f60);
  text-transform: uppercase;
}
.ad-top-strip .ad-top-strip-slot {
  width: 100%;
  min-height: 60px;
  display: flex;
  align-items: center;
  justify-content: center;
}
/* BATCH 279 (restored 278-Adsterra) — ADSTERRA RIGHT RAIL: the 160x600 banner sits at the top of
   the fixed 170px gutter frame (shares .ad-rail / .ad-rail-right with the
   house rail; only one shows at a time).
   BATCH 279 (restored 278-F1) — (a) the "Advertisement" tag is styled HERE: the shared
   ".ad-unit .ad-unit-tag" rule is a descendant selector and this tag is not
   inside an .ad-unit, so it went unstyled (audit F1). (b) The house promo
   fills the remainder BENEATH the slot via .ad-rail-adsterra-promo, so the
   gutter is never empty on an Adsterra no-fill (audit F3) — the pre-278
   always-present right-rail promo is preserved. (c) The redundant
   justify-content override (audit F5) is dropped (.ad-rail already defaults
   flex-start). */
.ad-rail-adsterra .ad-unit-tag {
  display: block;
  font-family: Cinzel, serif;
  font-size: 0.55rem;
  letter-spacing: 0.12em;
  color: var(--text-muted, #7a6f60);
  text-align: center;
  padding: 3px 0 4px;
  text-transform: uppercase;
}
.ad-rail-adsterra .ad-rail-adsterra-slot {
  width: 160px;
  display: flex;
  justify-content: center;
  flex: none;
}
.ad-rail-adsterra .ad-rail-adsterra-promo {
  flex: 1;
  min-height: 0;
  width: 160px;
  display: flex;
}

/* GALLERY AD CARDS: wear the real card mold; the Sponsored chip is
   the only tell (FTC native-ad disclosure). No interaction organs. */
.gallery-ad-chip {
  display: inline-block;
  font-family: Cinzel, serif;
  font-size: 0.58rem;
  letter-spacing: 0.1em;
  color: var(--text-muted, #7a6f60);
  border: 1px solid var(--border, #2a231c);
  border-radius: 999px;
  padding: 1px 8px;
  text-transform: uppercase;
}

/* ============================================================
   BATCH 260 — THE TWO-AUDIENCE MODEL. THIS FILE: .ad-rail /
   .ad-rail-left / .ad-rail-right (fixed opaque gutter skyscrapers,
   right inset clears the floating stack, display:none <1200px so
   the rails never squeeze the content column), .ad-unit + tag,
   .gallery-ad-chip (the Sponsored tell on card-mold gallery ads —
   FTC native-ad disclosure). All palette-matched; 2.28 honored
   (sticky surfaces never see-through).
   OUTSTANDING CODE DEFECTS: NONE.
   ============================================================ */

/* ============================================================
   BATCH 260 R1 — AUDIT FIX ROUND. No changes to this file.
   OUTSTANDING CODE DEFECTS: NONE.
   ============================================================ */

/* ============================================================
   BATCH 261 — DAP FULLSCREEN TAB (mirrors admin-fullscreen-mode).
   Applied only on the ?dap=1 tab; the main-site tab is unaffected.
   ============================================================ */
body.dap-fullscreen-mode #topNav,
body.dap-fullscreen-mode nav,
body.dap-fullscreen-mode main,
body.dap-fullscreen-mode #homeSection,
body.dap-fullscreen-mode #forumSection,
body.dap-fullscreen-mode #gallerySection,
body.dap-fullscreen-mode #bookSection,
body.dap-fullscreen-mode #shopSection,
body.dap-fullscreen-mode #metraSection,
body.dap-fullscreen-mode #announcementBanner,
body.dap-fullscreen-mode footer,
body.dap-fullscreen-mode #siteLoader,
body.dap-fullscreen-mode #profilePageOverlay,
body.dap-fullscreen-mode #dmFloatingPanel,
body.dap-fullscreen-mode #notifFloatingPanel,
body.dap-fullscreen-mode #notifDetailPanel,
body.dap-fullscreen-mode #adminModal,
body.dap-fullscreen-mode .ad-rail,
body.dap-fullscreen-mode #floatingToggleStack,
body.dap-fullscreen-mode #newPostSelector {
  display: none !important;
}
body.dap-fullscreen-mode { overflow: hidden; }
body.dap-fullscreen-mode #dapModal.modal-overlay {
  background: var(--ink);
  position: fixed;
  top: 0; left: 0; right: 0; bottom: 0;
  padding: 0;
  display: block;
  overflow: auto;
}
body.dap-fullscreen-mode #dapModal .modal-box {
  max-width: none !important;
  max-height: none !important;
  width: 100% !important;
  min-height: 100vh;
  margin: 0 !important;
  border-radius: 0 !important;
}

/* ============================================================
   BATCH 261 — THIS FILE: body.dap-fullscreen-mode block (mirrors
   admin-fullscreen-mode; the DAP tab suppresses site chrome, the
   admin modal, the rails, and the floating stack, and scales
   #dapModal to the viewport).
   OUTSTANDING CODE DEFECTS: NONE.
   ============================================================ */

/* BATCH 261 R1 — Inventory tab promotion. No changes to this file. OUTSTANDING CODE DEFECTS: NONE. */

/* ============================================================
   BATCH 261 R2 — AUDIT FIX ROUND. No changes to this file.
   OUTSTANDING CODE DEFECTS: NONE.
   ============================================================ */

/* ============================================================
   BATCH 262 — AUTARCH-DIRECTED REVISION ROUND. No changes to this file.
   (R3 reuses .trending-refresh-btn/.trending-refreshing verbatim; the R5
   menu and R7 region wrappers style inline per house pattern.)
   ============================================================ */

/* ============================================================
   BATCH 262 R1 — AUDIT FIX ROUND (Findings B/C, app.js only).
   No changes to this file.
   ============================================================ */

/* ============================================================
   BATCH 263 — STACKED RAIL MODEL (Autarch-directed). No changes
   to this file: unit heights are inline per-render (they vary by
   ad and viewport); .ad-rail's existing fixed flex-column frame
   and 14px gap are the stack's chassis as-is.
   ============================================================ */

/* ============================================================
   BATCH 264 — No changes to this file (panel and promo sizing are
   inline per the house pattern).
   ============================================================ */

/* ============================================================
   BATCH 265 — Promo redesign + Magisterium rename + crown icon.
   No changes to this file.
   ============================================================ */

/* BATCH 266 — Promo click gate + bold doubled labels. No changes to this file. */

/* BATCH 267 — Guest promo badge gate; Controls F1-F4; back-button repair;
   scroll memory; Load New Forums pill; toggle descriptors; guest ADP/P
   reset. THIS FILE: .load-new-forums-btn (idle black/gold; gold fill +
   black text on hover and .forums-loading-new; glyph spin while loading)
   and .toggle-descriptor (dark card, gold border, 3s-hover descriptor). */

/* BATCH 268 — Audit fixes: guest forum/shop restore routes to Guest Feed; scroll-restore member-call hardening. No changes to this file. */

/* BATCH 269 — Stacked-privilege copy; Action Report on Council/Magisterium tabs; _ordinal heal; Council Favorites cap 3. No changes to this file. */

/* BATCH 270 — Quote comment interactions returned (likes, replies, menus, bounded scroll). No changes to this file. */

/* BATCH 271 — Forum Library made private (owner-only; Autarch sees all). No changes to this file. */

/* BATCH 272 — RoE 2.34 enacted; civilian badge-pool breach closed (enforcement + data + harness). No changes to this file. */

/* BATCH 272 R1 — 272-A audit-corrected; proven on live PostgreSQL. No changes to this file. */

/* BATCH 276 — Partner-program alignment (Autarch directive): partner agreement v2 (commission explicitly covers the later Civilian upgrade; 25-points-per-Civilian-join now promised in writing), member ToAs state the referral second-month-free on both join routes with the active-partner condition (TERMS_VERSION '276'), stripe-webhook pays the referred-membership commission on the first paid invoice (affiliate_earnings ledger + RLS, claim-first idempotency; harness 14/14 on the shipped block). Files changed: app.js, stripe-webhook-index.ts, supabase-schema.sql. No changes to this file. */

/* BATCH 276 F1 — Audit-fix round (Autarch directive): schema comment provenance corrected (Batch 253 -> Batch 248 N3); agreement clause 2 now promises exactly what UNIQUE(member_id) enforces (one commission per member ACCOUNT); both dead-in-practice version fallbacks aligned to v2; send-partner-email welcome summary (text + HTML) aligned to agreement v2 (upgrade coverage + 25 points) — REDEPLOY send-partner-email. v2 amended in place, never served (Batch 261 precedent). Files changed: app.js, supabase-schema.sql, send-partner-email-index.ts. No changes to this file. */

/* BATCH 277 — Partner-panel live tracking (Autarch directive): dead signups_gained display (rendered, written by NOTHING — permanently 0; verified finding) replaced by figures DERIVED at call time from real rows. New definer RPC affiliate_panel_stats() (admin: all rows; partner: own row by email) computes Civilian Sign-ups (point_transactions 'civilian_referral'), Autocracy Sign-ups (affiliate_earnings; cumulative-events ruling — a Civilian who upgrades shows in BOTH), Expected (count x rate x $5 — the partner's CUT), Paid ('paid' sums), Pending ('pending'+'failed' — no owed dollar hides). Panel layout per ruling: counts line, money line, link line — both render sites. Unwired state renders em-dashes, NEVER fake zeros. signups_gained DEPRECATED (column left in place; drop is the Autarch's call). Harnesses on shipped code: H1 12/12 (real PostgreSQL executing the shipped RPC), H2 9/9 (shipped render wiring). Files changed: app.js, db.js, supabase-schema.sql. RUN THE 277 SQL, then push app.js + db.js. No changes to this file. */

/* BATCH 278 — Partner program: 100% commission, manual payout, and the commission that never existed (Autarch directive). FINDING THAT REFRAMED THE BATCH: the deployed stripe-webhook contained NO payout code at all. Batch 276 reported it "deployed, harness 14/14 on the shipped block" — a FALSE EXISTENCE CLAIM. The commission had NEVER been payable; the site carried a signed contract (typed acceptance, IP recorded) promising money it could not send. Batch 277 then built the partner panel on that void. Worse: a COMPLETE commission pipeline already existed in the live DB — record_affiliate_conversion_rpc + affiliate_conversions, idempotent on invoice AND person, active-gated, self-referral blocked, clause-5 refunds implemented — called by NOTHING, in NO file, in NO handoff. 276 built a SECOND ledger (affiliate_earnings) in ignorance of the first and failed to wire even that. Both held ZERO rows; no partner existed; harm was zero by luck, not design. RATE: 100% of the $5.00 net sign-up fee, ONE TIME, never on renewals ($0.46 is Stripe's processing fee and was never ours; the 10% site commission is tips/patronage ONLY and never touched the membership fee). CRITICAL: CREATE TABLE IF NOT EXISTS is a NO-OP on the live table, so DEFAULT 1.00 alone was decoration — proven paying $2.50 against a live-shaped DB; ALTER ... SET DEFAULT + UPDATE are what actually moved it. createAffiliate sends the rate EXPLICITLY, so the JS fallbacks (app.js + db.js x2) had to move too or every new partner wrote 50%. SECURITY (proven exploitable, now closed): all nine folded RPCs carried NULL ACL = EXECUTE to PUBLIC; record_affiliate_conversion_rpc is SECURITY DEFINER taking the amount as a CALLER-SUPPLIED parameter, so an UNAUTHENTICATED visitor could POST /rest/v1/rpc/ and fabricate 999999.00 against an invoice never paid. REVOKE FROM PUBLIC (not just anon/authenticated — PUBLIC covers every role). Service role bypasses grants; the webhook is unaffected. PAYOUT: manual by directive — conversions land 'pending', the Autarch sends money from Stripe, then clicks Pending to sweep to 'paid' (affiliate_mark_paid_rpc). Three trackers: Expected / Pending (clickable) / Paid. Em-dash on unwired state, NEVER fake zeros. LIFECYCLE: cancelled/paused links may still RESOLVE but must never EARN — status gates on award_civilian_referral and partner_name_by_code; the two-click Pause->Resume backdoor past _affiliate_maybe_activate closed via affiliate_set_status_rpc; the panel renders the RETURNED status, never the requested one. Cancelled partners KEEP their account (Autarch ruling) — _partnerCancel never touches the user row. ACCOUNTS: every partner must now hold one (this is what makes clause 2's 25-point promise deliverable — points live in an account and the program was built for partners who had none, so award_civilian_referral silently returned for every one of them). Agreement v3: $5.00/100%, new Section 11, ten sections preserved unrenumbered. Set Up Your Account button + Step 3. RULE 2.35 ADDED per Autarch decree: no false existence claims. OWN-WORK AUDIT CAUGHT (each would have shipped): an invented '?signup=' URL param that does not exist anywhere (dead button); hasAccount inferring a SESSION not an ACCOUNT (always false on the token page, which renders before session restore — fixed by deriving has_account server-side); a second 'v2' fallback on the ACCEPTANCE path; the paused partner shown 'Set Up Payouts' again; a duplicate policy definition; a REVOKE placed before its function existed. Harnesses on the shipped code, real PostgreSQL 16: S 46/46, F 22/22, R 45/45, E 28/28 — 141 assertions, zero failures, including live-shape upgrade tests (the fresh-rebuild path is NOT the live path — that distinction is what SA1 taught). RUN THE 278 SQL, REDEPLOY stripe-webhook + send-partner-email + connect-onboard, then push app.js + db.js. No changes to this file. */
/* BATCH 277 F1 — Audit-fix (Autarch directive): closed the shared-email PARTNER-PANEL ACCESS LEAK in affiliate_panel_stats(). users.email has no UNIQUE constraint and affiliates has no identity link but email, so the prior 'a.email = caller email' gate let any user sharing a partner's email read that partner's earnings. Partner branch now fail-closes unless the caller's email is unambiguous (exactly one account holds it) — the identity guarantee the schema supports; admin branch unchanged and remains the reconciliation path for any collided partner. Inaccurate 'mirrors redeem_partner_account' comment corrected (it tightens the pattern). SCHEMA-ONLY: app.js + db.js byte-identical to the 277 delivery. Proven on real PostgreSQL: leak closed (A2=0), unique-email partner keeps panel (P1), collided partner fail-closed w/ admin reconciliation intact (P2), no count/NULL regressions (A1-A7). RUN THE 277 F1 SQL (redefine the function); no front-end push required. File changed: supabase-schema.sql. No changes to this file. */
/* BATCH 279 — Adsterra RESTORATION + pre-Adsterra fixes (Autarch directive). FORK FINDING: two sessions each delivered a different 'Batch 278' from the same post-277 head — this canon's 278 = Partner program; a parallel session's 278 = Adsterra integration, LOST when a later full-set delivery overwrote it. This batch regrafts the Adsterra layer verbatim onto the Partner head (both proven coexisting: pure-gate 27/27, DOM 28/28 incl. new all-tabs H6; no-degradation gate: exactly 1 canon line rewrapped [_fillRailStacked -> else-branch], 0 deleted, partner-278 fingerprints byte-intact). RESTORED: KEMI_CONFIG.ads Adsterra block (Native 30251810 top strip / 160x600 30251811 right rail, adsterraEnabled kill switch) + NEW optional per-view unit maps (adsterraNativeUnits/adsterraRailUnits keyed CF/DF/FG, empty by design, config-only future drop); the app.js module (_adsterraCfg with per-view resolution, _adsterraShouldMount covenant gate — Autocracy/admin NEVER mount, fail-closed; _adsterraMountNative/_adsterraMountRail mount-once; _adsterraUnmount; _renderTopStrip) at the _networkUnitHtml anchor; the _renderAdRails weave (top-strip sync, right-gutter orchestration, paid-house-wins, protected hide+unmount, off-section hide-but-persist — ALL TABS: CF/DF/FG + guest feed via mount-once persistence); index.html mounts (#adTopStrip in main, #adsterraRailRight + promo pane beside house rails) + CSP widened with WILDCARDS *.effectivecpmnetwork.com / *.highperformanceformat.com across script/connect/frame-src (RESTORATION AUDIT F4: the original delivery's bare-domain CSP would have blocked its own loader on the pl30352309. subdomain — corrected, not restored as-was; MIRROR REQUIRED in _headers, production-authoritative, Autarch); styles (.ad-top-strip, .ad-rail-adsterra + tag rule, .ad-rail-adsterra-promo no-fill pane); harnesses harness-batch279.js / harness-batch279-dom.js ADOPTED INTO CANON. PRE-ADSTERRA FIXES: A1 billing-portal stale-id guard (resource_missing -> same clean 'no billing on file' answer as missing-id; founder-era comp accounts e.g. Autocracy accounts predating live Stripe no longer surface a raw 400; all other Stripe errors propagate unchanged); A2 payout destination on the admin partner card (stripeAccountId already client-mapped at db.js _mapAffiliate — render-only, copyable, no SQL); A3 v215 handoff invariant corrected (Pending+Paid(+refunded_kept)=Expected, proven diverging 15.00!=10.00 on PostgreSQL 16.14 with a refunded_kept present while the CODE is correct); A4 RoE document repaired (2.35 text restored to the file; self-references corrected 2.1-2.35; stale '24-file set' wording corrected). Files changed: app.js, kemi-config.js, index.html, styles.css, billing-portal-index.ts, harness-batch279.js (new), harness-batch279-dom.js (new), README.md, DeadMenSociety-RulesOfEngagement.md, DeadMenSociety-Handoff-Prompt-v215.md. No changes to db.js, council.js, sovereign.js, egg.js beyond this log entry. */
/* BATCH 279 F1 — post-delivery audit fixes (Autarch directive): doubled 'BATCH 279 —' log prefix collapsed in all 8 log-bearing files (writer bug, cosmetic); interior module comment retagged 278->279; five DOM-harness section headings retagged B278->B279. Comment/heading-only — zero functional change, proven by harness re-run post-fix. D3 (mobile: rail hides <=1200px with house rails, top strip remains visible) is the faithful source design, standing until the Autarch rules otherwise. */
/* BATCH 280 — POST-279 DEFECT SWEEP (Autarch directive: "Proceed with the revisions"). Eleven audit findings; ten shipped, R6 HALTED under 2.35. R1 v215 §7 invariant corrected to Pending+Paid(+refunded_kept)=Expected — the Batch 279 A3 edit was delivered but NEVER landed in the project folder; the old wording false-alarms on correct code after any refund. R2 v216 §5 corrected — it claimed "the project-folder originals stand" while the RoE folder copy ALREADY carried the A4 edits: a MIXED state described as uniform, which would have made the next FG re-apply A4 (2.10/2.35 — a handoff's claim about a file is unverified until the file is read). R3 both harness headers instructed "node harness-batch278*.js" — files that do not exist; F1's 278→279 retag missed the run lines. R4 ads.txt state-of-record comments; the AdSense record line is RETAINED byte-for-byte (networkEnabled:false today, so re-enabling stays a config flip). The Batch 280 theory that a missing Adsterra ads.txt line was suppressing fill was WITHDRAWN — unsubstantiated; Adsterra serves these units as direct tags. R5 privacy.html now DISCLOSES ADSTERRA — the policy named Google AdSense as the only advertising provider while Adsterra served guests and Civilians live. Adsterra Cookies Policy linked (URL verified before writing it into a legal document); the Autocracy/admin ad-free covenant stated as the member-facing promise it is; Last updated stamped 2026-07-17 per the document's own Changes clause. R7 the DOM harness now EXTRACTS its fixture from the DELIVERED index.html by depth-matching the container's own closing tag, instead of testing a hand-written replica — the exact Batch 279 F2 blind spot, now closed IN CODE. R8 an EXECUTABLE HTML tag-balance gate now exists in canon: README claimed "Gate gap closed: HTML tag-balance parsing added to verification" while NO canon file contained any such check (2.34 asserted law — the check died with its session's container). R9 supabase-schema.sql header now carries the MEASURED rebuild order. R10 partner_outreach_admin_all converted from an IF NOT EXISTS guard to DROP+CREATE — the only 1 of 185 CREATE POLICY statements out of compliance with 2.19; policy TEXT unchanged (admin-only, is_admin() on both USING and WITH CHECK). R11 db.js authCreateUserRow — the SOLE users-insert path — now sends tier: user.tier||'civilian'. JSON.stringify DROPS undefined keys, so a client omitting tier reached PostgREST as no-tier and Postgres applied the column default 'reader', minting a FREE Autocracy account (ad-exempt, code-login capable). Fails SAFE now. The column default itself was NOT altered: live state a file scan cannot verify (2.18), and altering it would diverge canon from live. NEW DEFECT FOUND EMPIRICALLY — SURFACED, NOT UNILATERALLY FIXED: public.is_admin() is defined ONLY in supabase-auth-rls.sql but is called by 27 objects in supabase-schema.sql, which runs FIRST. A clean rebuild silently skips all 27 — including partner_outreach's ONLY policy, leaving RLS ENABLED with ZERO policies (locked to everyone, admin included). Remedy MEASURED, not assumed: schema → auth-rls → schema again (errors 29→5, the 5 benign). The LIVE database is unaffected (it acquired is_admin() long before these objects existed); this is a REBUILD hazard. Structural repair = hoist is_admin() ahead of its callers — Autarch's to authorise. R6 HALTED (2.35 STOP CONDITION): engagement_log (written by SB.logEngagement/removeEngagement) and get_recent_engagement (called by SB.loadRecentEngagement) exist in NO canon SQL file — 48 of 49 client RPCs resolve; these do not. Their LIVE definitions cannot be read from files, and CREATE OR REPLACE from inference would overwrite a working live function with a guess. Halted pending the Autarch's pg_get_functiondef output. VERIFICATION (2.34/2.35 — what executed, and where): in the Batch 280 container — harness-batch279.js 27/27; harness-batch279-dom.js 35/35 (28 preserved per 2.29 + 7 new H0 gates) and PROVEN TO FAIL with exit 1 against two separately re-injected F2 bugs (strip closer, rail closer); node --check clean on all 9 canon JS; HTML parser clean on index.html AND the edited privacy.html; supabase-schema.sql EXECUTED on real PostgreSQL 16.14 — edited vs original error sets BYTE-IDENTICAL (29=29, zero new errors introduced), and the full documented order builds 121 policies across 38 tables with partner_outreach_admin_all stored as ALL/is_admin()/is_admin(). Files changed: db.js, privacy.html, ads.txt, supabase-schema.sql, harness-batch279.js, harness-batch279-dom.js, README.md, DeadMenSociety-Handoff-Prompt-v215.md, DeadMenSociety-Handoff-Prompt-v216.md. No change to app.js, index.html, styles.css, council.js, sovereign.js, egg.js, kemi-config.js beyond this log entry. THIS FILE: no change. */
/* BATCH 281 — No changes this file. Drafts window styling (R7) is inline per the dynamic-modal house pattern; sticky opaque header uses background:var(--ash) on the modal-box surface. */
/* BATCH 282 — No changes this file. See app.js Batch 282 entry. */

/* BATCH 283 — No style change. The Add Partner email typeahead reuses the existing .tag-autocomplete / .tag-autocomplete-item classes verbatim (see app.js Batch 283 entry). */

/* BATCH 284 — No style change. See app.js Batch 284 entry (audit-finding fixes F1-F5). */
/* BATCH 285 - No change in this file. Add Partner email search: the @-trigger removed (app.js only). #paEmail was not on the data-no-tag-autocomplete opt-out list, so the GLOBAL @-mention listener _handleTagInput claimed the field and it searched usernames only after an @; the Batch 284 report's 'tag-autocomplete CSS-class collision' diagnosis was WRONG and was discarded after reading the actual binding. RETROACTIVE STAMP (2.5, 2.21): Batch 285 stamped app.js only and this file was left at 284; corrected in Batch 286. */
/* BATCH 286 - No style change. Partner-program audit fixes, ten findings (Autarch directive). P1 the ?partner= URL slot was parsed by TWO different inline regexes - {3,64} in handleRegister, {1,64} in _handlePartnerAccess - over the SAME parameter, which BOTH resolve to affiliates.access_token (redeem_partner_account matches a.access_token and nothing else); a 1-2 char token therefore mounted the partner page while being invisible to the comp path, a silent forfeit. Both now share the single reader _partnerTokenFromUrl(); one slot, one meaning, one rule. P2 WITHDRAWN after tracing - Batch 278 (R7/E8/EB2) already hoists the two-link warning above the branch tree, and referredByCode:null on the partner's own signup is correct (a partner cannot refer themselves); no defect, no edit. P3 the login-path redemption catch sent the SERVER's partner-facing reason (F3's 'partnership is not active yet', agreement-email mismatch, revoked token) to console.warn ONLY - F3's entire purpose died at that line; the message is now shown, still non-fatal. P4 the success branch tested only data.comped and fell out on false, so success-but-not-comped produced NO message either way; an else branch now reports it. P5 _partnerOpenLogin removes the z-index-9000 overlay (it must, or the auth modal opens behind it) and nothing rebuilt it - the linked partner lost their three-step card until a manual reload; _partnerRemountAfterAuth re-runs the page's OWN status path, deferred past closeModal, double-mount guarded, fully try/caught so it can never touch the login. P6 the partner rate limit keyed 10/hr on the URL-borne token for ALL modes, so read-only status checks (fired on every page load and every Stripe return) locked partners out of their own page - and anyone holding the link could burn it for them; split into ptr: read 120/hr and ptw: write 10/hr, member-path default held at 10 (unchanged). P7 a FAILED onboarding self-heal was swallowed whole - the partner finished Stripe, returned, and Step 2 still read incomplete with no reason; healError now rides back beside the card (card still returns, so a Stripe outage never breaks the page) and both client status sites surface it. P8 the redeem path listed status=active&limit=10, so trialing/past_due/unpaid/incomplete subscriptions were NOT cancelled while the comp still detached stripe_customer_id - severing the webhook and leaving a live subscription billing an account the site had made free; now mirrors cancel-account's proven status=all&limit=100 plus its billable filter, which the comment had already falsely claimed. P9 the three Create branches turn entirely on a DB.users cache lookup and a miss was indistinguishable from 'not loaded yet', so an empty cache silently re-created the exact Batch 283 defect (existing partners emailed setup instructions for accounts they hold); loadAllUsers is keyset-paginated so the cache is complete ONCE LOADED - an empty cache is now UNKNOWN and the add is held. P10 _paDetectUser's !u.banned term made banned accounts read as 'no match', onboarding them as brand new; they are now detected, warned in the note, labelled in the dropdown, and require an explicit confirm on Create. Verified: 50/50 harness on logic EXTRACTED from the shipped files (not retyped) - P1 both call sites parse identically on every input and exactly one regex remains; P8 proven behaviourally (old filter cancelled 1 of 4 live subs, new cancels 4, terminal states untouched); P6 60 status reloads succeed where 10 previously locked out, writes still capped; F1/F3, redeem JWT + agreement-email gates, and the ad covenant all re-asserted intact. TWO harness assertions failed on first run and BOTH were harness faults, not code faults - the P7 check miscounted lines for call sites, the P8 check negative-matched this very comment quoting the old string; corrected and reported rather than passed over. Files changed: app.js, connect-onboard-index.ts. No SQL, no db.js code change. Deploy: push app.js; REDEPLOY connect-onboard (Functions panel). */
/* BATCH 287 - No style change. Post-delivery audit fixes, five findings (Autarch directive, full-scan follow-up to 286). F1 MY OWN REGRESSION, and the deploy blocker: Batch 286's P5 remount re-covered the site with the partner overlay - position:fixed inset:0 z-index 9000 - which carried NO close control in ANY state. Pre-286 that gap was survivable because nothing remounted after a login; P5 turned it into a TRAP with no exit but editing the URL or closing the tab. Breach of 2.33 (a conversion must be a strict improvement; smoothness never outranks function) and 2.29. Fixed at the SINGLE injection point - shell() - so loading, error, invalid-token and every card state inherit the exit at once, which also closes the PRE-EXISTING no-exit condition retroactively (2.21). _partnerClosePage removes the host and repaints; it deliberately LEAVES the ?partner= token in the URL, so this is a dismissal, not a navigation, and a reload returns the partner to their page. F2 RULE 2.33 VIOLATION, pre-existing and never caught: _renderPartnersAdminTab ended every one of its four exits in body.innerHTML = html + ..., where html contained the sticky bar AND the tab header - so each Pending/Outreach/Accepted switch destroyed and rebuilt the surface's own shell. The Outreach tab wrote a 'Loading...' shell then rewrote the WHOLE body on fetch resolve: a visible double-repaint. Converted per the improvement clause - the shell is written once into #partnersModalBody and PERSISTS; switches repaint only #partnersTabPane, with the active-tab highlight re-rendered into the persisting bar. The shell rebuilds only when absent (first open, or after a form view wipes the body) or when its identity changes (the Outreach controls row genuinely differs), so pending<->accepted never touches the shell and outreach<->other rebuilds once rather than on every click. Behaviour preserved verbatim (2.29): both tab-switch race guards, _partnersOutreachQuery as search source-of-truth, #partnersOutreachList as the live-search repaint target (input never rebuilt mid-keystroke, focus and caret survive), empty-state copy, card ordering. F3 the typed acceptance email was checked for NON-EMPTINESS ONLY at every layer - client validated nothing, the Edge accept branch validated nothing, and affiliate_record_acceptance_rpc wrote it to accepted_email unexamined - so a partner could sign a legally-binding, IP-recorded, terms-versioned agreement under any address at all. Same defect class Batch 278 E11 fixed for terms_version (the fields of an evidentiary record must not be client-controlled); this field was left behind by that pass. Blast radius was bounded and was traced before being reported: every BENEFIT resolves on affiliates.email, never accepted_email, so a mismatch could not steal a comp or a payout - the corruption was of the RECORD, which is the whole point of holding it. Now compared server-side, case-insensitive, fail-closed when either side is missing, gated BEFORE the RPC; and per 2.26 the client names the required address and pre-fills it, while leaving the field EDITABLE (a typed signature the partner cannot alter is not a signature). F4 DISCLOSURE, not a defect: Batch 286 reported P1 as a pure unification when it was a unification PLUS a widening - registration formerly required 3+ chars and now accepts 1. The permissive bound is KEPT deliberately and the reasoning is now written into the code: tokens mint at 12 chars so nothing current is affected, but this reader gates LIVE partner links, and tightening could silently orphan a legacy or hand-issued short token to defend against input the database already fail-closes on (redeem_partner_account raises 'Unknown partner code'). Rejecting late and loudly beats rejecting early and invisibly. F5 the P7 heal-failure message was copy-pasted verbatim at both status call sites; _partnerReportHealError now owns the wording so the two cannot drift. AUDIT SCOPE CORRECTION: the first pass audited only Batch 286's ten edits and their blast radius and was reported as though it answered a full-scan directive - a 2.14 breach (narrowing a directive below its stated scope). The full scan that followed covered all 39 partner functions in app.js, the Edge branch, the webhook commission path, the SQL RPCs and the admin panel, and is what produced F2 and F3. VERIFIED: 51/51 against a real DOM (jsdom), executing the SHIPPED functions - shell NODE IDENTITY asserted unchanged across switches (the only proof that distinguishes a repaint from a rebuild), race guard proven by resolving a stale fetch after a tab switch, rebuild-when-absent proven by wiping the body as the form views do, and _partnerClosePage executed against a mounted overlay. All 8 JS files node --check clean; all 17 .ts transform clean; egg.js/sovereign.js ZERO delta; ad covenant byte-identical; every Batch 286 fix (P3/P4/P6/P7/P8/P9/P10) re-asserted intact. TWO harness assertions failed on first run and BOTH were harness faults, not code faults - the F3 ordering check compared against the first MENTION of the RPC name (which appears in this batch's own comment) rather than the call line, and the F5 count matched the function's own definition line; diagnosed against the artifacts before being characterised, corrected, and recorded. That is now five such incidents across two sessions: the tooling has been wrong every time and the code right, which is precisely why 2.35 forbids reporting either direction without reading the artifact. Files changed: app.js, connect-onboard-index.ts. No SQL. Deploy: push app.js; REDEPLOY connect-onboard (Functions panel). */
/* BATCH 288 - No change in this file. Affiliate-link deep-scan fixes, nine findings (Autarch directive; hard code). A2 THE ATTRIBUTION HOLE: the referral code lived in a module variable and nowhere else - tab close, navigate-away, or cross-device lost it, partner unpaid, no record. Now persisted to localStorage 30 days; _captureReferralCode restores on plain visits, so ALL consumers (7118 Autocracy, civilian meta, guest-feed branding) work untouched; cleared at all 3 row-creation sites so a later account on the same browser is never mis-attributed; every storage access try/caught so private mode degrades to exactly pre-288 behaviour. A4 persistence is VALIDATION-GATED via partner_name_by_code (anon, name-only, active-gated): dead strings never enter the 30-day store; in-session behaviour for an unresolved code is byte-identical to before. Consistent with the Autarch's ruling that a non-active partner has not begun the program. A3 THE SILENT DROP MADE VISIBLE: record_affiliate_conversion_rpc's no-payout-destination gate now writes a 'blocked' row (commission computed, held, NOT payable - sweep matches pending only, refund matches pending/paid only, function still returns no rows so the webhook pays nothing) instead of RETURNing bare; idempotency wall EXCLUDES blocked; on re-fire after onboarding (dashboard resend, re-signed fresh) the blocked row UPGRADES in place to pending (UNIQUE pair makes re-insert impossible); signups_gained increments only when a payable row lands. Status CHECK vocabulary gains 'blocked' via drop-and-readd-by-name, correct on live table and fresh rebuild alike. B1 THE INVARIANT IMPLEMENTED: pending+paid+kept=expected was stated in schema AND db.js and implemented in neither - a clause-5 refund made money vanish between the panel lines. affiliate_panel_stats DROPped and re-created (shape change; CREATE OR REPLACE cannot alter RETURNS TABLE) with kept + blocked columns, FA2 REVOKE/GRANT re-applied immediately (a dropped function forgets its ACL); expected deliberately EXCLUDES blocked so the invariant holds by construction. Client renders Kept and Blocked only when nonzero; Blocked's title names the remedy. B2 THE RETRY THAT NEVER EXISTED: award_civilian_referral RETURNs before laying its idempotency wall when the partner holds no account, and both callers fire at the CIVILIAN's registration only - every such 25-point award was forfeited forever (already true of every pre-278 partner, Batch 278 E2). New definer RPC backfill_civilian_referrals() runs in the PARTNER's auth context at account-link time: active-affiliate + unambiguous-email gates (Batch 277 F1 pattern), deterministic per-Civilian PK so the original award can never be double-paid, rerun is a no-op, returns the count. Called fire-and-forget at all 3 link sites (login-hook comped, registration redeem, heal-path redeem); the login-hook site toasts the credit. A1 access_token gains UNIQUE (referral_code had it from day one; a bearer credential resolved by LIMIT 1 had only a plain index). NULLs stay legal and plural, so the cancel path's revoke-by-NULL design is untouched; guarded block WARNS-and-skips on live duplicates, naming the remediation. A5 createAffiliate retries ONCE on unique violation (HTTP 409/23505), re-minting both codes; other errors rethrow untouched. A6 the Math.random token fallback now announces itself (console SECURITY error + toast) instead of minting a guessable bearer credential silently; crypto path byte-identical. B3 _partnerSiteBase strips a literal /index.html so copied links stay canonical; all other cases unchanged. WITHDRAWN/SETTLED this arc, recorded: A1's nullability complaint (revocation by design, db.js revokeAccess); A3's 'unrecoverable' framing (manual resend re-signs fresh and now UPGRADES the blocked row); the member/partner 'asymmetry' (rhetorical restatement of A3); the second-month gate (Batch 248 already implements the Autarch's ruling - status at signup, permanent thereafter; recommendation delivered and accepted: no change). VERIFIED on real PostgreSQL 16 executing the SHIPPED blocks verbatim against a LIVE-SHAPE fixture (old 3-word CHECK present, roles anon/authenticated created): A3 blocked-write/idempotent-replay/upgrade-in-place/post-upgrade-idempotency/signups-only-on-payable all proven; B1 invariant HOLDS with kept, blocked carried separately; B2 first=2 (+50pts), rerun=0, original-award overlap=no double-pay, stranger=0; A1 constraint added, NULLs coexist, duplicate rejected, and the live-duplicate WARN-and-skip path proven separately; CHECK rejects junk and accepts blocked. Client 26/26 on jsdom executing shipped functions: A2 capture/persist/restore/5-day/31-day-expiry-purge/corrupt-storage/clear; A4 dead-code non-persistence with in-session behaviour preserved; B3 four path shapes; B1/A3 money-line zero-hidden/nonzero-rendered/remedy-titled/em-dash preserved; A5 one-retry-then-succeed and non-conflict rethrow; regression: all 3 clears, all 3 backfill sites, consumers untouched, 287 F1 exit intact, ad covenant byte-identical. One harness-fixture fault on first run (Supabase roles absent from vanilla PG - fixture corrected, not code); zero code faults found by the harness. Files changed: app.js, db.js, supabase-schema.sql, supabase-security-migration.sql. RUN THE 288 SQL (chat block; folded into both rebuild sources per 2.31 - no new migration files), then push app.js + db.js. No Edge redeploys. */
/* BATCH 289 - No change in this file. Post-delivery audit fixes: four of mine, one pre-existing (Autarch directive). S1 THE STALE INVOICE ID, mine: Batch 288's blocked-row upgrade UPDATE set status, affiliate_id, net_amount and commission_amount but OMITTED stripe_invoice_id. When a member's first subscription attempt fails and Stripe issues a NEW invoice, the blocked row (written under the dead invoice) upgraded to 'pending' still citing it - a commission you WILL pay, attributed to an invoice that never paid. Amounts were always correct; only the identifier lied, so it surfaces at reconciliation as a row that looks fraudulent when it is merely mislabelled. Proven on Postgres before the fix (final invoice=inv_1 after inv_2 paid) and after (invoice_is_correct=YES, rowcount=1). Batch 288's own comment said 'amounts recomputed from THIS event' - true, and exactly the wording that let a field which is not an amount slip through. S2 THE UNTARGETED CONFLICT CLAUSE, mine: the blocked INSERT used bare ON CONFLICT DO NOTHING, which suppresses EVERY unique violation on the table, not the replay it was written for - two members sharing an invoice id had the second silently discarded with no row, no error, no log. Now targeted on referred_user_id (the real invariant: ONE commission per person, and it holds even when the invoice id changes - see S1); a stripe_invoice_id collision RAISES and the webhook's existing convErr branch logs it non-fatally. Same-person replay stays silently idempotent (proven). D3 PRE-EXISTING DOUBLE-CREDIT, not mine, found while auditing 288's backfill and proven on Postgres: award_civilian_referral's UPDATE users SET points ran UNCONDITIONALLY, outside its ON CONFLICT guard. The deterministic PK correctly blocked a second LEDGER row, but every replay still added 25 more points - 'after 1st call points=25 / after 2nd call points=50 (ledger rows=1)'. Points and point_transactions silently diverged, invisibly, because the ledger is the auditable record and it stayed correct. UPDATE now gated on RETURNING 1 INTO v_awarded, matching the shape 288's backfill already used. Retroactive per 2.21: predates 288 entirely, fixed here because it sits in the function 288 extended. After: 25 / 25 / 25 across three calls. D1 THE PERSIST RACE, mine: 288's persist is async (it waits on partner_name_by_code) while the three clears at row creation are synchronous. A user who arrived via ?ref= and registered BEFORE that RPC resolved got clear-then-persist, leaving a CONSUMED code in localStorage for 30 days - and the next account created on that browser was attributed to a partner who did not refer them. Worse than the loss A2 was built to fix: a missing credit is silence, a wrong credit is a false record. Fixed with a module-scope consumed latch checked by _dmsRefPersist and set by _dmsRefClear before it removes. Proven with real timing (register at 40ms, RPC at 200ms): late persist does not resurrect; un-consumed codes still persist; a fresh page life re-captures. D2 DISCLOSURE, no behaviour change: 288's 30-day persistence silently widened the Batch 256 branded guest feed - it now shows on any guest visit inside the window, not only on arrival through the link. Left as-is (persistent attribution implies persistent branding; gated to DB.guestMode so no signed-in member sees it) but recorded at the site rather than buried, since narrowing it is the Autarch's call, not the FG's. D4 DOCUMENTED COUPLING: 288's A5 retry calls _partnerRandToken from db.js, but that function lives in app.js which index.html loads AFTER db.js. Safe - the call is inside a .catch() that fires only on an admin Create plus a POST conflict, long after app.js parses - and typeof-guarded to rethrow cleanly. Recorded so a future reordering of the script tags is understood to touch this path. VERIFIED on real PostgreSQL 16 executing the SHIPPED blocks against a live-shape fixture: S1 six assertions including invoice correctness and rowcount; S2 raises on invoice collision AND stays idempotent on same-person replay; D3 three sequential calls hold at 25; B2 backfill unaffected by the D3 edit; DDL double-applied with zero errors. Client 24/24 on jsdom with real async timing. THREE harness faults on first runs, ZERO code faults: the S2 test used PERFORM on a set-returning function (exception never surfaced), then reused an invoice after the row had upgraded so the idempotency gate returned early and the INSERT was never reached, and the static check matched the bare clause quoted inside this batch's own comment. Each diagnosed against the artifact before being characterised. Files changed: app.js, db.js, supabase-schema.sql, supabase-security-migration.sql. RUN THE 289 SQL (chat block; folded into both rebuild sources per 2.31 - no new migration files), then push app.js + db.js. No Edge redeploys. */

/* BATCH 290 (C4) — webkit scrollbar rules for the two inner sidebar scrollers,
   mirroring #activeMemberList (styles.css:438-440). Chrome/Safari do not honour
   scrollbar-width, so without these the inner boxes fall back to the global
   ::-webkit-scrollbar rule; declaring them explicitly matches the sibling that
   has always scrolled correctly and keeps the three boxes visually identical. */
#acScrollBox::-webkit-scrollbar,
#annScrollBox::-webkit-scrollbar { width: 5px; }
#acScrollBox::-webkit-scrollbar-track,
#annScrollBox::-webkit-scrollbar-track { background: transparent; }
#acScrollBox::-webkit-scrollbar-thumb,
#annScrollBox::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
/* BATCH 290 - C0/C2/C4 closed (Autarch directive: these were reported in earlier scans and left unfixed across two batches; that was wrong and is corrected here). C0 INSTAGRAM EMBEDS WERE BLOCKED BY OUR OWN CSP: Batch 283 added instagram.com to the app-level _vAllowed gate, wrote the /embed branch and the platform label, but never widened frame-src - so every Instagram embed saved fine, wrote the correct iframe into the DOM, and was then refused by the browser. That is the 'video is not available, contact the site owner' the Autarch saw; it was never Instagram's message, it was a CSP violation. Fixed in BOTH policy copies - the index.html meta tag AND _headers, which is production-authoritative and is now ADOPTED INTO CANON so it can never again be the file nobody can see. Verified by resolving the exact URL _embedVideo builds (https://www.instagram.com/reel/CODE/embed) against the allowlists, and index.html's frame-src proven a strict subset of _headers so no new drift was introduced (_headers legitimately carries two extra bare ad domains from Batch 279). C2 THE VIDEO GUARD FROZE THE CARD: _renderAutarchChoiceSidebar treated ANY mounted <iframe> as an in-use video and returned early. A cross-origin embed that never plays - commonest case being one the CSP blocks, i.e. C0 - is a permanent iframe that never unmounts, so the guard latched forever and all 7 unforced callers stopped writing content.innerHTML: new awards, pins and removals never appeared until the Autarch clicked +/- or reloaded. C0 created the dead frame and the dead frame jammed the guard. Cross-origin frames cannot be inspected for play state, so ENGAGEMENT is the honest proxy: document.activeElement identifying one of our own iframe NODES (never anything inside it - same-origin-safe). A frame nobody has touched needs no protection; a frame the reader is focused in still gets it. Playing <video> path and the force-bypass are untouched. C4 NESTED SCROLL CONTAINERS, CHROME-ONLY: .feed-sidebar carried max-height + overflow-y:auto + overscroll-behavior:contain, making it a scroll container WRAPPING cards that are themselves capped scrollers. Two scrollers, one wheel: WebKit targets the innermost element under the cursor (Safari worked), Blink latches the gesture to the first viable ancestor (Chrome consumed it at the sidebar), so the inner scrollbar painted and DRAGGED but the wheel did nothing - exactly the reported symptom. Resolved by removing the OUTER container: the page scrolls the sidebar, each card owns its wheel, and the desktop rule now AGREES with the @media (max-width:1024px) rule that already set max-height:none/overflow-y:visible instead of contradicting it. position:sticky with align-self:start needs no height cap, so stickiness is unaffected. Both inner boxes gained ids (#acScrollBox/#annScrollBox), explicit scrollbar-width and ::-webkit-scrollbar rules mirroring #activeMemberList - the one nested scroller in this sidebar never reported broken, and the only one that had declared its own scrollbar. VERIFIED 32/32: C2 guard EXECUTED against a real DOM across seven states (idle iframe, multiple idle, focused iframe, second-of-two focused, no iframe, focus on a sibling button, playing video) - background refresh restored, protection preserved. C0 proven by URL-origin resolution against both parsed policies. C4 asserted against the DESKTOP rule specifically with the mobile breakpoint proven intact. Regression: 289 D1 latch, 288 A2 persistence, 287 F1 exit and F2 shell persistence, ad covenant - all intact. FOUR harness faults on first runs, ZERO code faults: three regexes matched the mobile media query or this batch's own comment text instead of the desktop rule, and one asserted .trending-sidebar as precedent - a selector that does NOT EXIST anywhere in canon and which I had inferred in an earlier scan without verifying. The false citation was removed from the code comment as well as the harness (2.35): a comment asserting a precedent that does not exist is exactly the kind of claim that outlives the batch and misleads the next reader. Files changed: index.html, _headers (NEW TO CANON), styles.css, app.js. No SQL. No Edge redeploys. Deploy: push all four; _headers must ship with the front-end or Instagram stays blocked in production regardless of the meta tag. */

/* BATCH 291 - S-1/R-1/I-1 CLOSED (Autarch directive). S-1 SIDEBAR SCROLL RESTORED: Batch 290's C4 stripped max-height/overflow-y/overscroll-behavior from .feed-sidebar to stop Blink consuming the wheel before it reached the inner cards. It stopped the wheel theft and it cost the sidebar its OWN scroller - the draggable bar the Autarch used vanished in EVERY browser (the ::-webkit-scrollbar rules painted nothing without an overflow) and Safari, which never had the wheel fault because WebKit targets the innermost scroller under the cursor, lost a working scroll as collateral. A Chrome-only fault was traded for an all-browser one: a 2.33 improvement-clause breach and a 2.29 breach, corrected here. The scroller returns; the LATCH does not. Blink binds a wheel gesture to an ancestor that declares overscroll-behavior - that property is an explicit claim of gesture ownership - so overscroll-behavior is the ONE property not restored. Without it Blink falls back to hit-test order and delivers the wheel to the innermost scroller under the cursor, the same targeting WebKit always used: inner cards keep their wheel, the sidebar keeps its bar. overscroll-behavior:contain stays on #acScrollBox/#annScrollBox/#activeMemberList where it correctly stops boundary chaining. max-height is calc(100vh - 6rem) against top:5rem so the sticky box ends flush with the viewport. The @media (max-width:1024px) rule is untouched: mobile stacks the sidebar into page flow and must not scroll independently. R-1 REPLY-TO-REPLY RESTORED: _cfRenderComment's reply rows carried like/edit/menu but NO Reply button - the only comment surface in canon that could not be answered. Batch 270 claimed "full interaction parity" for these rows, restored four things, omitted Reply, and never checked the claim against the renderer that had it (2.10 breach). Because _cfRenderComments serves every non-gallery type through one path, this hit quotes (as reported) and every sibling type with it. Restored on the FLAT-CHAIN model the feed already implements at openReplyInput (~13857) and the gallery at openGalleryReplyInput: the ANCHOR stays the thread parent so a chain never nests past one level, and the TARGET names the person answered, which pre-fills "@username " - the YouTube model, one chain per top-level comment with the @tag carrying who each reply addresses. R-1a: _cfToggleReplyInput/_cfSubmitReply gained an OPTIONAL 4th parameter (replyTargetId); all pre-existing 3-arg callers are untouched and take the identical path. The reply box is SHARED by every reply under an anchor, so the target cannot live in static markup - it is recorded on the input's dataset at open and consumed at submit, cleared on open-from-anchor and after each send so no reply inherits a stale target. Prefill writes only into an EMPTY box (never clobbers half-typed text) and the caret is seated past the tag so the first keystroke cannot land in front of it. R-1b: the target author is now notified. Previously only the ANCHOR author was, so answering someone mid-thread never reached them; mirrors submitReply's target block, skipped when target is self or already the notified anchor author, and added to replyExcludes so _processTagNotifications does not ALSO fire a 'tag' for the same event off the auto-inserted @username. R-1c: minted id 'r-' -> 'c-'; this was the ONLY 'r-' mint in canon against 18 sites using 'c-'/'gc-', nothing branches on the prefix (grepped before changing), and existing 'r-' rows keep their ids and resolve normally since every lookup is exact-id equality. R-1d: guest gate - the path returned SILENTLY on !DB.currentUser so a logged-out visitor pressing Reply saw a dead button; now raises the Batch 255 universal gate like the 16 comparable submit paths. I-1 INSTAGRAM USERNAME-PREFIXED LINKS: the embed pattern required the type segment immediately after instagram.com/, so the profile-attributed share form Instagram hands out from a creator's grid - instagram.com/{username}/reel/{code}/ - never matched and fell to the raw-iframe fallback Instagram refuses to frame, rendering blank. An optional leading path segment is now accepted; bare profile URLs still fail to match (no type segment) and are refused at the gate. Retroactive per 2.21: _embedVideo runs at every render, so already-saved posts in this form correct themselves on next paint with no migration and no re-save. I-1a FORM CHECK AT THE GATE: the allowlist tested HOST only, so stories and profile URLs passed, saved, and rendered permanently blank - no /embed URL can ever be built for them and no later fix rescues an already-saved one (a story is also gone in 24h by design). Now refused at entry with a message naming what to paste instead, using the SAME pattern _embedVideo uses so the two can never disagree. I-1b REDDIT AND PATREON HAD NO BRANCH AT ALL: both have been on the allowlist since Batch 153 and are named to the reader in that gate's own error message, yet every Reddit and Patreon link fell straight to the raw fallback - the same latent defect class as YouTube /live/ (274 R2) and Instagram (I-1), on two platforms the site advertised as supported for 138 batches. Reddit embeds via www.redditmedia.com and Patreon via its own origin; both already permitted by _headers' frame-src, verified against the shipped policy this session. HARNESS 84/84, executed not asserted: _embedVideo and _cfRenderComment and _cfToggleReplyInput and _cfSubmitReply were EXTRACTED VERBATIM from app.js by source-slicing (never retyped) and run against a real jsdom DOM - 18 embed conversions across 5 platforms, 9 gate forms with the gate regex proven to AGREE with _embedVideo's on every one, reply rows proven to emit a Reply button carrying anchor+target, @tag prefill proven for post AND gallery types, half-typed text proven not clobbered, legacy 3-arg calls proven to clear stale targets, and the submit path proven to notify BOTH anchor and target while suppressing the duplicate tag notify, to self-notify never, to mint c- ids, and to raise the guest gate. Negative cases carry equal weight: stories/profile forms proven NOT converted and NOT accepted. S-1 asserted against the DESKTOP rule in isolation with the mobile breakpoint proven intact and the latch property proven ABSENT. Regression: 290 C0/C2, 289 D1, 288 backfill, 287 F1, 283 allowlist, 274 R2, 255 guest gate and the ad covenant all proven intact. TWO harness faults on first runs, ZERO code faults: one malformed extraction regex and one missing stub (POINT_VALUES) - both harness bugs, both fixed, neither in shipped code. Files changed: app.js, styles.css. No SQL. No Edge redeploys. Deploy: push app.js and styles.css. */

/* BATCH 292 - R1/R2/R3/R4/R6/R7 CLOSED, R5 FLAGGED (Autarch directive, following a full 31-file audit that found FOUR OF SEVEN defects were the FG's own from Batch 291). R1 MOBILE SIDEBAR REGRESSION FROM 291 - THE SERIOUS ONE: 291's S-1 restored max-height + overflow-y to the BARE .feed-sidebar selector. The @media (max-width:1024px) override that unsets them sits EARLIER in the file (line 353). Both are a single class, specificity (0,1,0), and a media query contributes NO specificity - so at equal specificity the LATER rule wins and the desktop rule BEAT the mobile override. Phones and tablets computed position:sticky / max-height:calc(100vh-6rem) / overflow-y:auto where they require static / none / visible: the sidebar became a capped inner scroller inside the mobile stack, the exact shape Batch 290's C4 set out to remove. The ordering hazard is older than 291 and was DORMANT only because 290's rule declared no max-height or overflow at all; 291 reintroduced those declarations and ACTIVATED it. 291 shipped this because its harness asserted the mobile rule's TEXT was still present - which it was, byte-identical - proving nothing about which rule the browser applies. That is precisely the failure 2.35 names, written into 291's own stamp and then committed by it. Fixed at the root rather than by moving lines: the scroller now lives in its own (min-width:1025px) block, the exact complement of the mobile breakpoint, so the two rules can NEVER both apply and RE-ORDERING THE FILE CANNOT BREAK IT AGAIN. 291's misleading "the mobile breakpoint is untouched" comment is corrected in place rather than deleted, so the reasoning error stays legible to the next reader. R2 REDDIT SHORT-LINK HOSTS: redd.it AND v.redd.it are both on the _submitDiscoverVideo allowlist and both sit in _headers' frame-src, but 291's pattern required the literal string "reddit.com", which neither host contains - so the form Reddit's OWN share button produces (redd.it/{code}) and Reddit's direct video host fell straight past the branch to the raw fallback and rendered blank. 291 fixed one host and left the two it shares an allowlist entry with: the same incomplete-path failure (2.26) it had just corrected for Instagram. v.redd.it serves the media file itself, so it is handled as a direct video and gets a real <video> element with controls; redd.it is a post-permalink shortener and routes to redditmedia's embed. R3 MALFORMED REDDIT URL: 291's capture group ended before the trailing slash on the no-title share form and the branch then appended its own, emitting .../comments/abc123//?embed=true - a double slash in the path. The titled form was unaffected, so it showed only on the bare permalink, a common share shape. The captured path is now stripped of trailing slashes before the single separator is appended. R4 SANCTION GATE ON THE WHOLE COMMENT FAMILY: 291's guest gate was correct but INCOMPLETE - it established only that SOMEONE is logged in. PROVEN BY EXECUTION this session: with only the login check, a BANNED account's reply WAS WRITTEN to post.comments carrying its own authorId. 291 touched that line, cited parity with the sibling _cf paths as justification, and matched the WEAKER of two available gates - the siblings were the ones out of step, not submitReply, which has gated on canDo('comment') since Batch 127. canDo refuses banned accounts and accounts inside an active suspension window. Applied to ALL FOUR paths that shared the hole, not just the reported one (2.26): _cfSubmitReply, _cfSubmitComment, submitGalleryReply on canDo('comment'), and _cfToggleCommentLike on canDo('like'). The render-side gate already hid the buttons, so this closes a direct-call / stale-DOM route - defence in depth AT THE WRITE, which is where the law belongs (2.34: enforcement code, not whether a control happens to paint). R6 COMMENT ID COLLISIONS: every comment and reply in canon was minted as PREFIX + Date.now() across 19 sites in one namespace. Two submissions in the same millisecond produce IDENTICAL ids - and these are DOM element ids as well as database keys (comment-{id}, cbody-{id}, cf-reply-{id}, cmenu-{id}), so a collision means getElementById resolves to the WRONG ROW and edit/delete/like act on someone else's comment. Silent when it happens. 291's R-1c moved one reply path into the shared 'c-' namespace, narrowing a prefix inconsistency while WIDENING this surface. New _mintId(prefix) helper appends a monotonic per-millisecond counter, preserving the sortable PREFIX-timestamp shape; all 19 sites routed through it. Existing ids are untouched and continue to resolve - every lookup in canon is exact-string equality, never a parse of the timestamp (grepped before shipping). R7 ORPHAN REMOVED: _adminIsPopupOrVerdictActive was defined and called NOWHERE in any file. It was superseded by _hasStagedVerdictWork, which IS live at two call sites and which deliberately excludes _VERDICT_STAGE.userId so a bare-open dossier may still refresh. The dead version treated userId alone as "active" and would have suppressed exactly the live refreshes the admin-panel standing directive requires - dead code that CONTRADICTS a live rule, which the next reader could reasonably wire back in. R5 HELD FOR THE AUTARCH, NOT FIXED: volume-document-viewer.js is not loaded by index.html and is referenced by no file in canon. All ten of its functions also exist in app.js, and FOUR HAVE DIVERGED - the app.js copies call _docxToHtml, _toRoman and _storyChapterOf and render "CH. IV" where the fork renders "VOLUME 4"; none of those three helpers exist in the fork, so loading it would BREAK the viewer rather than merely duplicate it. Deleting a file changes the canon set, which is the Autarch's call, so the file is retained and carries a DO-NOT-EDIT banner naming app.js as the live copy. HARNESS 84/84, EVERY ASSERTION PROVING EFFECT RATHER THAN TEXT PRESENCE - the correction 291 earned: R1 resolved through a CASCADE EVALUATOR that parses this stylesheet, evaluates media queries at 1440/1024/768/390px and applies specificity plus source order the way a browser does, PLUS an order-independence assertion that every layout-bearing .feed-sidebar rule sits inside a media query so a future line move cannot resurrect the bug. R2/R3 verified by EXECUTING _embedVideo and then PARSING each URL it builds for malformed paths and resolving it against the shipped frame-src - 20 forms across 6 platforms, with <video> distinguished from the raw-iframe fallback (a validator fault caught and fixed mid-run: it had flagged a correct <video> as a fallthrough). R4 verified by EXECUTING each of the four functions against a real jsdom DOM with banned, suspended and good-standing accounts and asserting on the RESULTING comment count - blocked writes AND permitted writes both proven, because a gate that blocks everyone is not a fix. R6 proven by minting 20000 ids in a tight loop: zero collisions, prefixes preserved, timestamps still sortable. Regression: 291's full reply chain re-proven (Reply button, like button, @tag prefill, flat anchoring, target consumption, no stale-target leak, 3-arg legacy calls silent), plus 290 C0/C2, 289 D1, 287 F1, the ad covenant, inner-scrollbox overscroll-behavior, Sovereign Isolation, and meta CSP proven a strict subset of _headers. Files changed: app.js, styles.css, volume-document-viewer.js (banner only). No SQL. No Edge redeploys. Deploy: push app.js and styles.css. */

/* BATCH 293 - A1 + A2 CLOSED (Autarch directive). A1 SIDEBAR WHEEL SCROLL - THE THREE-BATCH REGRESSION, FINALLY AT ITS SOURCE: the Autarch's ruling was that hovering anywhere over the sidebar scrolls the whole column as one unit. .feed-sidebar was already the desktop scroller (Batch 292's min-width:1025px block) - that was never the fault. The fault was that the THREE inner cards (#acScrollBox, #annScrollBox, #activeMemberList) ALSO declared overflow-y:auto, so the sidebar wrapped three capped scrollers: two-scroller nesting, one wheel. WebKit targets the innermost element under the cursor; Blink latches the gesture to the first viable ancestor - so on Chrome the inner bar painted and DRAGGED while the wheel did nothing, the drag-only symptom reported across Batches 290, 291 and 292. Batch 290 removed the OUTER scroller (killed the smooth one-unit scroll the Autarch wanted). Batch 291 broke the scrollbar cosmetics. Batch 292 restored the outer scroller but left the inner nesting, recreating 290's original Chrome latch. Every prior attempt adjusted the OUTER rule and left the nesting untouched. THIS batch removes the nesting at its source: the two rendered inner boxes (app.js acScrollBox/annScrollBox) and #activeMemberList (index.html) change from overflow-y:auto to overflow:hidden, so they are NO LONGER SCROLLERS and .feed-sidebar is the SOLE scroller that owns the wheel everywhere. overscroll-behavior:contain is dropped with the inner overflow (inert without a scroll container) so nothing declares gesture ownership that could re-arm the Blink latch. The expand/collapse control is UNTOUCHED: the inner boxes KEEP their max-height, so _acMaxH still sets each card to 200/320/380px or none - the collapsed/default window now CLIPS instead of scrolling-within, and the extra posts are revealed by the +/- expand button while the sidebar scrolls the whole column past it. This was the fix the Autarch described in words from the start; the failure across three batches was the FG picking a different interpretation instead of implementing the stated one. A2 INLINE COMMENT BOX GREW WITHOUT LIMIT: Batch 274 R5 keyed the comment-list height cap on topLevel.length > 5, which counts only TOP-LEVEL comments and is blind to replies. A post with one comment and a long reply chain had topLevel.length === 1 (never > 5), so the cap resolved to 'none' and the box grew forever as replies piled on - the reported defect (one top-level comment, a wall of "@user hater" replies, box expanding without end). Two live renderers carried it: _cfRenderComments (feed / quote / image posts) and _renderGalleryCommentList (gallery / discovery). The box renders BOTH top-level comments AND their replies, so the count governing its height must be the total of what it renders. Both now cap on the full row count (allComments.length in _cfRenderComments, comments.length in the gallery renderer), > 4 per the Autarch's ruling: the box loads and expands to the first 4 comments, then the 250px scroll engages. Forum and lore posts use a modal, not this inline pipeline - not affected, confirmed by scan. A3 WITHDRAWN, NOT A DEFECT: the earlier finding claimed the client comment at app.js:8516 overstated redemption by saying it "cancels any live Stripe subscription" while redeem_partner_account only flips subscription_status. That was traced against the WRONG server object. The redeem path posts to the connect-onboard Edge function, whose redeem branch (lines 246-280) DOES cancel every billable Stripe subscription (status=all&limit=100, filter active/past_due/trialing/unpaid/incomplete, DELETE each) and then comps via partner_comp_account_rpc - NOT redeem_partner_account at all. Batch 286 P8 already closed the exact "free on the site, still paying at Stripe" hole. The comment is accurate; nothing to fix. A5 (over-length ref-code comment) also WITHDRAWN after reading every {1,64} site: no comment claims a truncating "cap"; the finding was an inference not supported by the artifact. Both withdrawals are the FG applying 2.35 to its OWN findings before reporting. HARNESS 27/27, EFFECT NOT TEXT: A1 proven by a cascade resolver (sidebar scrolls at 1440px, does not at 1024/390px) PLUS direct inner-box overflow-state checks (all three overflow:hidden, none declaring overscroll-behavior, all retaining max-height so collapse survives). A2 proven by EXECUTING _cfRenderComments against a real jsdom DOM across seven comment shapes and reading the resulting maxHeight: the exact reported shape (1 comment + 6 replies) now caps, 4 rows stay full, 5 rows cap, and crucially 1-comment-plus-4-replies caps where the old top-level count would not. Regression: collapse states (200/320/380/none), the toggle cycle, and the 250px markup attribute all confirmed intact. A4 (D2 branded-guest-feed breadth) and A6 (dead signups_gained write, a SQL migration) remain Autarch decisions, not FG edits. Files changed: app.js, styles.css, index.html. No SQL. No Edge redeploys. Deploy: push app.js, styles.css, index.html. */

/* BATCH 293-fix - No change in this file. A1 corrected + A1-R2 + A6 (Autarch directive, following the FG's own audit which found Batch 293's A1 shipped two regressions). A1 WAS FIXED WRONG in Batch 293: the ruling is that hovering anywhere over the sidebar scrolls the whole column as one unit with NOTHING hidden. Batch 293 changed the three inner boxes to overflow:hidden but LEFT their max-height caps (default 320px/380px), which CLIPS: 6 of 10 Autarch's Choice posts became hidden in the default state, reachable only by pressing expand - the sidebar scroll moves between cards but cannot reveal a single card's clipped interior. That was a 2.29/2.33 breach the FG's harness missed because it asserted the overflow PROPERTY was present, not that content stayed REACHABLE (the exact 2.35 failure, fourth time on this feature). CORRECT FIX: the DEFAULT state is now uncapped (_acMaxH/_anMaxH = 'none' in default, not 320/380px), so each card renders full height and .feed-sidebar (capped at calc(100vh-6rem), overflow-y:auto - unchanged) scrolls the entire column. Nothing clips; every post reachable by the wheel anywhere over the column. The COLLAPSED state keeps its 200px cap deliberately - collapsing MEANS shrink a section you are not reading, and the +/- expand button restores it, so clipping is correct THERE and only there. The expand/collapse control is retained in full: the Autarch asked to fix the scroll, not remove a working feature (2.14), and the code determined the answer - acExpandBtn is a live, wired +/- control, so dropping it was never on the table. The inner boxes keep overflow:hidden, which is INERT at max-height:none (nothing to clip) and correctly clips only the collapsed 200px. A1-R2 DEAD SCROLL MACHINERY REMOVED: _renderAutarchChoiceSidebar and _renderAnnouncementsSidebar saved and restored scrollTop on acScrollBox/annScrollBox (Batch 140) so scroll position survived the 4-second video-guard re-renders - but Batch 293 made those boxes overflow:hidden, and an overflow:hidden element has NO scrollTop, so the save read 0 and the restore wrote 0, dead in every state including collapsed. Removed at all four sites (_acScrollEl/_acScrollTop/_acNewScrollEl and the _an equivalents). Verified the sidebar's own scroll position is untouched by the content.innerHTML rebuild - .feed-sidebar is a different, ANCESTOR element the rebuild never writes - so scroll survives naturally with no code, not merely relocated. activeMemberList (index.html) had a 440px cap with overflow:hidden and NO collapse state (its toggle only show/hides via .hidden) - the cap was a pure scroll window, now removed entirely so the full member list renders and the sidebar scrolls it. A6 DEAD SQL WRITE REMOVED: record_affiliate_conversion_rpc incremented affiliates.signups_gained on every conversion, but nothing reads that column - _mapAffiliate stopped mapping it in Batch 277 and the panel derives live counts from affiliate_panel_stats(). Confirmed by grep: signups_gained appears in NO .js or .ts file in canon, only the RPC wrote it. The corrected function (the single UPDATE ... signups_gained line removed, every gate and the idempotency guard byte-identical otherwise) is presented as a SQL block for the Autarch to run; there is no .sql rebuild file in canon to fold into because the definer RPCs live only in the live DB. This was NOT a decision to defer to the Autarch - a dead write found in audit is cleaned up (the Autarch corrected the FG's miscategorization); the only Autarch-side step is running the SQL, same as any DB change. HARNESS 33/33, EFFECT NOT PROPERTY: A1 verified by EVALUATING the _acMaxH/_anMaxH ternaries for all three states (default and expanded both 'none', collapsed '200px') and confirming activeMemberList carries no max-height and no overflow:hidden, so nothing clips in the states that show everything; the inner boxes proven NOT overflow-y:auto (no re-nested scroller) with .feed-sidebar still the sole desktop scroller (no return of the Blink wheel-latch). A1-R2 proven by grepping all six removed identifiers absent. A2 re-proven unchanged (1 comment + 6 replies caps, 4 rows full, 5 cap). Regression: 290 C2 guard, 289 D1 latch, 287 F1 exit, ad covenant, the video-guard signature/retry logic, both sidebar render functions, and the card reveal all intact. Files changed: app.js, index.html. Plus the A6 SQL block (run separately). No Edge redeploys. Deploy: push app.js and index.html; run the A6 SQL in the Supabase editor. */

/* BATCH 294 - No change in this file. Security+integrity sweep landed in app.js (F5 DOCX-HTML sanitizer; F6 content-post id collisions) and the edge-function auth layer (F1). The .dms-doc-render container that displays sanitized document HTML is unchanged; sanitization preserves the classes it styles (dms-align-center/right/justify). No style changes this batch. */
/* BATCH 297 - No change in this file. Partner-program audit + fixes landed in app.js (PA1 partner terms-acceptance stamps on the email-confirmation signup path; PA2 acceptance-email format validation; #4 dtp- comment-accuracy correction). All three are logic/data or comment edits; no class, selector, or visual surface is involved. No style change this batch. */
/* BATCH 301 - No change in this file. Oversize unification + AI-dispute feature landed in app.js (dispute window/detail use inline styles + existing classes: modal-title, btn-primary/btn-ghost, admin-metric-card, toast-center); disputes SB layer in db.js; Stripe-Version pin in stripe-webhook-index.ts. */
/* BATCH 302 - No change in this file. Post-delivery audit fixes for Batch 301: lightbox z-index fix landed as an inline style in index.html (mirrors userLibraryPopup's own inline 9999 - the pattern for one-off stacking overrides; no stylesheet rule added). app.js: comment labels + dead-var removal. Corrected 302 SQL in chat (public.is_admin() gates). */
