/*
 * VILLAGE SOUL — styles.css
 *
 * Parchment + ink + one blood-red accent.
 * Lora for chronicle serif, IBM Plex Sans for UI, IM Fell for display.
 *
 * Authored: Computer (Perplexity AI agent) 2026-05-06
 */

:root {
  /* The palette: warm parchment, dark ink, blood-red accent.
     Deepened in PR #7 to match the IMG_3396–9 mockup spec —
     the parchment now reads as an aged sheet, not a tinted webpage. */
  --parchment: 38 38% 86%;            /* #efe4cc — deeper warm cream */
  --parchment-deep: 36 32% 80%;       /* slightly darker for cards */
  --parchment-edge: 32 28% 64%;       /* torn-edge shadow */
  --matte: 28 35% 9%;                 /* dark warm wood-tone behind the sheet */
  --ink: 30 22% 11%;                  /* near-black with warm tint */
  --ink-soft: 30 18% 25%;             /* paragraph color */
  --ink-faint: 30 18% 32%;            /* secondary text — darkened in G-1 (PR-G1) to meet WCAG 1.4.3 AA (4.5:1 against parchment) */
  --accent: 0 58% 32%;                /* blood / sealing-wax red */
  --accent-soft: 0 45% 50%;           /* warmer accent */

  --serif: "Lora", Georgia, serif;
  --sans: "IBM Plex Sans", system-ui, sans-serif;
  --display: "IM Fell English", "Lora", Georgia, serif;
}

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

html, body {
  height: 100%;
  width: 100%;
}

body {
  font-family: var(--serif);
  color: hsl(var(--ink));
  /* Dark warm matte behind the parchment sheet — see .page-frame below */
  background: hsl(var(--matte));
  background-image:
    radial-gradient(ellipse at 50% 50%, hsl(28 30% 14%) 0%, hsl(var(--matte)) 70%);
  /* P8 (Phase 3) — dropped `background-attachment: fixed`: it pins the
     gradient to the viewport, which on iOS Safari triggers a full-screen
     repaint on every scroll and disables GPU compositing for the body's
     layer. The gradient is large/soft enough that scrolling-with-content
     is visually indistinguishable. */
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

#app {
  min-height: 100vh;
  width: 100%;
}

.screen {
  width: 100%;
  min-height: 100vh;
  display: flex;
  align-items: flex-start;
  justify-content: center;
  padding: 3rem 1.5rem;
}
@media (max-width: 640px) {
  .screen { padding: 1rem 0.6rem; }
}

.hidden {
  display: none !important;
}

/* The parchment sheet itself — every screen's content lives inside one of these.
   This is the core visual primitive of the manuscript redesign:
   - Aged parchment surface with subtle warm/cool blotching and grain
   - Feathered torn edges via layered radial-gradient + box-shadow
   - Drop shadow onto the dark matte behind
   - Subtle SVG turbulence noise at very low opacity (deferred to PR #7.1 if needed) */
.page-frame {
  /* dc-Remix #12: parchment tone drift. Read --run-shade-hue and
     --run-shade-light (set by applyRunShade in ui.js) and shift the
     base parchment by these offsets. Defaults are 0 — the parchment
     reads identically on Days 1–8 of every run. From Day 9 it begins
     to shift; by Day 12 it's at full strength. The shift is below the
     'Instagram filter' threshold but above the 'feels different'
     threshold for replayers. */
  --run-shade-hue: 0deg;
  --run-shade-light: 0%;
  --run-shade-strength: 0;
  width: 100%;
  max-width: 1100px;
  position: relative;
  background:
    /* Warm/cool age blotching */
    radial-gradient(ellipse at 18% 12%, hsl(calc(38deg + var(--run-shade-hue)) 38% calc(90% + var(--run-shade-light)) / 0.7) 0%, transparent 45%),
    radial-gradient(ellipse at 82% 88%, hsl(calc(32deg + var(--run-shade-hue)) 28% calc(78% + var(--run-shade-light)) / 0.55) 0%, transparent 55%),
    radial-gradient(ellipse at 65% 30%, hsl(calc(36deg + var(--run-shade-hue)) 35% calc(84% + var(--run-shade-light)) / 0.4) 0%, transparent 40%),
    radial-gradient(ellipse at 30% 70%, hsl(calc(34deg + var(--run-shade-hue)) 30% calc(82% + var(--run-shade-light)) / 0.35) 0%, transparent 50%),
    /* Edge darkening (vignette) */
    radial-gradient(ellipse at 50% 50%, transparent 55%, hsl(var(--parchment-edge) / 0.45) 95%),
    /* Base parchment color — also shifted */
    hsl(calc(34deg + var(--run-shade-hue)) 35% calc(86% + var(--run-shade-light)));
  transition: background 0.6s ease;
  padding: 3rem 3rem 3.5rem;
  border-radius: 3px;
  box-shadow:
    /* Soft inner edge bleed (suggests torn fibers) */
    inset 0 0 32px hsl(var(--parchment-edge) / 0.35),
    inset 0 0 4px hsl(28 25% 35% / 0.18),
    /* Drop shadow onto the dark matte */
    0 8px 28px hsl(0 0% 0% / 0.45),
    0 24px 60px hsl(0 0% 0% / 0.35);
}
@media (max-width: 880px) {
  .page-frame { padding: 2rem 1.6rem 2.4rem; }
}
@media (max-width: 640px) {
  .page-frame {
    padding: 1.4rem 1rem 1.8rem;
    border-radius: 2px;
    /* Mobile root-fix: at narrow widths the vignette darkening (radial-gradient
       on background + inset box-shadow) sat directly behind the body text,
       killing contrast for the chronicle prose. Push the dark edge outward and
       soften the inset bleed so the text area stays on cream parchment. */
    background:
      radial-gradient(ellipse at 18% 12%, hsl(38 38% 90% / 0.7) 0%, transparent 45%),
      radial-gradient(ellipse at 82% 88%, hsl(32 28% 78% / 0.55) 0%, transparent 55%),
      radial-gradient(ellipse at 65% 30%, hsl(36 35% 84% / 0.4) 0%, transparent 40%),
      radial-gradient(ellipse at 30% 70%, hsl(34 30% 82% / 0.35) 0%, transparent 50%),
      /* Edge darkening pushed from 55%/95% to 75%/99% with lower opacity */
      radial-gradient(ellipse at 50% 50%, transparent 75%, hsl(var(--parchment-edge) / 0.22) 99%),
      hsl(var(--parchment));
    box-shadow:
      /* Halve the inset bleed so it doesn't darken under the text */
      inset 0 0 18px hsl(var(--parchment-edge) / 0.18),
      inset 0 0 2px hsl(28 25% 35% / 0.10),
      0 6px 18px hsl(0 0% 0% / 0.4),
      0 18px 40px hsl(0 0% 0% / 0.3);
  }
}

/* ─────────────────────────────────────────────────────────────────────────
   Typography
   ───────────────────────────────────────────────────────────────────────── */

.display-title {
  font-family: var(--display);
  font-size: 3.5rem;
  font-weight: 400;
  letter-spacing: -0.01em;
  line-height: 1.1;
  text-align: center;
  color: hsl(var(--ink));
}
.display-title.small {
  font-size: 2.25rem;
}

.subtitle {
  font-family: var(--serif);
  font-style: italic;
  font-size: 1.05rem;
  color: hsl(var(--ink-soft));
  text-align: center;
  margin-top: 0.5rem;
}

.section-label {
  font-family: var(--sans);
  text-transform: uppercase;
  letter-spacing: 0.18em;
  font-size: 0.78rem;
  font-weight: 500;
  color: hsl(var(--ink-faint));
  text-align: center;
  margin: 2.5rem 0 1.25rem;
}

/* ─────────────────────────────────────────────────────────────────────────
   Word picker
   ───────────────────────────────────────────────────────────────────────── */

.opening-frame {
  margin: 1rem 0 2.5rem;
  text-align: center;
}

/* Ornamental crest above the Village Soul title — IMG_3396 spec.
   Single inline SVG, currentColor. Sits above the title, opacity tuned
   so it reads as a manuscript ornament rather than a logo. */
.opening-crest {
  width: 44px;
  height: 44px;
  color: hsl(var(--ink) / 0.7);
  display: inline-block;
  margin-bottom: 0.4rem;
}
@media (max-width: 640px) {
  .opening-crest { width: 36px; height: 36px; }
}

.word-grid-section {
  margin-bottom: 1rem;
  scroll-margin-block-start: 80px;
}

/* Picker council decision (2026-05-13): the .picker-grid--columns
   dual-column wrapper is removed. The previous design rendered the
   14-word grid TWICE (once per word slot) inside this two-column
   container. Now we render the grid ONCE under a single
   .word-grid-section--single, and the typography + chip affordance
   carries the wordA/wordB sequencing. The old .picker-grid--columns
   rule is kept around as inert in case any other screen ever references
   it; the picker itself no longer produces a node with that class. */
.word-grid-section--single {
  margin-bottom: 1rem;
  max-width: 760px;
  margin-left: auto;
  margin-right: auto;
}

/* NH-A — tonal-cluster picker (general UX Tier 1 #2).
   The 14 words are grouped under 3 chronicler-voice cluster headings
   ("Of the warm hand", "Of the wandering hand", "Of the cold hand")
   so the player meets gentler options first and the picker feels
   organized without reading as a settings screen. Heading style is
   smaller-caps italic in faint ink, sitting on the same parchment
   as the buttons rather than rising above them as section dividers. */
.tonal-cluster {
  margin-bottom: 1.4rem;
}
.tonal-cluster-label {
  font-family: var(--serif);
  font-style: italic;
  font-weight: 400;
  font-size: 0.95rem;
  color: hsl(var(--ink) / 0.6);
  text-align: center;
  letter-spacing: 0.02em;
  margin: 1rem 0 0.6rem;
}
.tonal-cluster .word-grid {
  margin-bottom: 0;
}

/* Picker council decision (2026-05-13): word grid responsive layout.
   - Desktop ≥ 900px:  3 columns (was repeat(2,1fr) in the duplicated
     design; with a single grid we can fit 3 columns at the same outer
     width and the words sit at the right scale).
   - Tablet 560–899px: 2 columns.
   - Mobile  <560px:    1 column (the 14 words become a single vertical
     stack with the 28-card duplication gone). */
.word-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: 0.7rem;
  max-width: 760px;
  margin: 0 auto;
}
@media (min-width: 560px) {
  .word-grid { grid-template-columns: repeat(2, 1fr); }
}
@media (min-width: 900px) {
  .word-grid { grid-template-columns: repeat(3, 1fr); }
}

.word-button {
  font-family: var(--serif);
  background: transparent;
  border: 1px solid hsl(var(--ink) / 0.2);
  border-radius: 2px;
  padding: 0.85rem 0.9rem;
  text-align: left;
  cursor: pointer;
  display: flex;
  flex-direction: column;
  gap: 0.2rem;
  color: hsl(var(--ink));
  transition: all 0.15s ease;
}

.word-button:hover {
  border-color: hsl(var(--ink) / 0.5);
  background: hsl(var(--ink) / 0.04);
  transform: translateY(-1px);
}

/* Selected state per IMG_3396: deep red border + soft inner glow that
   fades from the border inward (not just a solid hairline). */
.word-button.selected {
  border-color: hsl(var(--accent));
  background: hsl(var(--accent) / 0.06);
  box-shadow:
    inset 0 0 0 1px hsl(var(--accent) / 0.55),
    inset 0 0 18px hsl(var(--accent) / 0.18),
    0 0 0 1px hsl(var(--accent) / 0.25);
}

.word-name {
  font-family: var(--display);
  font-size: 1.25rem;
  font-weight: 400;
  letter-spacing: 0;
}

.word-tagline {
  font-family: var(--serif);
  font-style: italic;
  font-size: 0.85rem;
  color: hsl(var(--ink-soft));
  line-height: 1.35;
}

/* ─────────────────────────────────────────────────────────────────────────
   Picker typography rebalance (2026-05-13)
   ─────────────────────────────────────────────────────────────────────────

   Council decision (tools/council/picker_typography_decision.md):
   - Heading 56px → 40/36/30 (desktop/tablet/mobile), SCOPED to #screen-picker
     so the sigil-ritual and chronicle screens that share .display-title are
     not affected (GPT condition 10).
   - Word-name 20px → 28/26/24, also scoped.
   - Card padding scales with the new word size.
   - Selected cards gain a 2px accent border, a slight parchment-darker
     fill, and a Roman-numeral chip (I / II) in the top-right corner that
     marks which slot (wordA or wordB) the selection fills. This is the
     load-bearing affordance once the duplicated grid collapses.
   - Modern-hand reading mode keeps the same scale (the IM Fell English
     swap to IBM Plex Sans loses some weight; we add a slight bump
     specifically in modern mode).
*/

#screen-picker .display-title {
  font-size: 2.5rem;       /* 40px desktop */
  letter-spacing: 0.005em;
}
@media (max-width: 899px) {
  #screen-picker .display-title { font-size: 2.25rem; }    /* 36px tablet */
}
@media (max-width: 559px) {
  #screen-picker .display-title { font-size: 1.875rem; }   /* 30px mobile */
}

#screen-picker .opening-crest {
  width: 32px;
  height: 32px;
}
@media (max-width: 559px) {
  #screen-picker .opening-crest { width: 24px; height: 24px; }
}

#screen-picker .word-name {
  font-size: 1.75rem;      /* 28px desktop */
  line-height: 1.1;
}
@media (max-width: 899px) {
  #screen-picker .word-name { font-size: 1.625rem; }       /* 26px tablet */
}
@media (max-width: 559px) {
  #screen-picker .word-name { font-size: 1.5rem; }         /* 24px mobile */
}

#screen-picker .word-tagline {
  font-size: 0.9rem;       /* 14.4px every viewport */
}

#screen-picker .word-button {
  padding: 1.1rem 1.15rem;
  gap: 0.3rem;
}
@media (max-width: 559px) {
  #screen-picker .word-button { padding: 1.25rem 1.15rem; }
}

/* Selected-state amplification: 2px accent border (was 1px), a soft
   parchment-darker fill, plus the existing inset blood-red glow. Border
   weight does the work; we do NOT grow the card size (that would cause
   grid reflow on selection, which the chronicle voice would never
   tolerate — the chronicle never resizes a word to signal weight; it
   reaches for blood-red ink). */
#screen-picker .word-button.selected {
  border-color: hsl(var(--accent));
  border-width: 2px;
  background: hsl(var(--ink) / 0.06);
  box-shadow:
    inset 0 0 0 1px hsl(var(--accent) / 0.55),
    inset 0 0 18px hsl(var(--accent) / 0.18),
    0 0 0 1px hsl(var(--accent) / 0.25);
  /* Compensate for the +1px border so the card doesn't shift in the grid */
  margin: -1px;
}

/* Roman-numeral chip on selected cards. Marks which slot (wordA = I,
   wordB = II) the selection fills. The chip uses IM Fell English on a
   blood-red circle, parchment-colored numeral — entirely within the
   locked palette. Sits slightly off the corner of the card like a
   sealed mark on a manuscript. */
#screen-picker .word-button.selected[data-pick]::before {
  content: attr(data-pick);
  position: absolute;
  top: -10px;
  right: -10px;
  width: 24px;
  height: 24px;
  border-radius: 50%;
  background: hsl(var(--accent));
  color: hsl(var(--parchment));
  font-family: var(--display);
  font-size: 0.9rem;
  line-height: 24px;
  text-align: center;
  letter-spacing: 0;
  font-weight: 400;
  box-shadow: 0 1px 3px hsl(var(--ink) / 0.25);
  pointer-events: none;
}
#screen-picker .word-button {
  position: relative;       /* anchor for the ::before chip */
}
@media (max-width: 559px) {
  #screen-picker .word-button.selected[data-pick]::before {
    width: 22px; height: 22px; line-height: 22px; font-size: 0.85rem;
    top: -8px; right: -8px;
  }
}

/* Focus-visible: explicit outline so keyboard users tabbing through
   14 cards in sequence get an unambiguous focus ring. The accent red
   is locked palette. (Opus condition 7.) */
#screen-picker .word-button:focus-visible {
  outline: 2px solid hsl(var(--accent));
  outline-offset: 2px;
}

/* Modern-hand reading mode: IM Fell English swaps to IBM Plex Sans,
   which is more condensed. Bump the word-name a notch so the choice
   reads at parity. (GPT missed risk 3.) */
html[data-reading-mode="modern"] #screen-picker .word-name {
  font-size: 1.875rem;     /* 30px desktop */
}
@media (max-width: 899px) {
  html[data-reading-mode="modern"] #screen-picker .word-name {
    font-size: 1.75rem;    /* 28px tablet */
  }
}
@media (max-width: 559px) {
  html[data-reading-mode="modern"] #screen-picker .word-name {
    font-size: 1.625rem;   /* 26px mobile */
  }
}

/* dc-Remix #6: word "tells" — the first two behavioral signatures per
   word, revealed on hover (with a 350ms delay) or on first tap on touch
   devices. The tells are the most evocative writing in the game; they
   should surface at the moment of decision.

   Implementation: collapsed by default via max-height: 0 + opacity: 0.
   The grid-template-rows: 0fr approach failed because grid children's
   intrinsic min-content kept the rows from collapsing to 0px. max-height
   is the more reliable affordance for a content-driven reveal. */
.word-tells {
  display: block;
  max-height: 0;
  overflow: hidden;
  opacity: 0;
  margin-top: 0;
  transition: max-height 0.4s ease 0s, opacity 0.3s ease 0s, margin-top 0.3s ease 0s;
}
.word-tells .word-tell {
  display: block;
  font-family: var(--serif);
  font-style: italic;
  font-size: 0.78rem;
  line-height: 1.45;
  color: hsl(var(--ink-faint));
  padding: 0.18rem 0 0;
}
.word-tells .word-tell::before {
  content: "— ";
  color: hsl(var(--ink) / 0.35);
}
/* Desktop: hover delay 350ms per the doc. Also reveal on keyboard focus. */
.word-button:hover .word-tells,
.word-button:focus-visible .word-tells,
.word-button.tells-revealed .word-tells,
.word-button.selected .word-tells {
  max-height: 120px; /* generous — two ~21px lines fit easily */
  opacity: 1;
  margin-top: 0.15rem;
  transition: max-height 0.4s ease 0.35s, opacity 0.3s ease 0.35s, margin-top 0.3s ease 0.35s;
}
/* Touch path: no hover delay (interaction is the tap itself). */
@media (hover: none) {
  .word-button:hover .word-tells { max-height: 0; opacity: 0; margin-top: 0; }
  .word-button.tells-revealed .word-tells,
  .word-button.selected .word-tells {
    max-height: 120px;
    opacity: 1;
    margin-top: 0.15rem;
    transition: max-height 0.3s ease, opacity 0.2s ease, margin-top 0.2s ease;
  }
}
@media (prefers-reduced-motion: reduce) {
  .word-tells { transition: none; }
  .word-button:hover .word-tells,
  .word-button:focus-visible .word-tells,
  .word-button.tells-revealed .word-tells,
  .word-button.selected .word-tells { transition: none; }
}

.word-button[data-tone="dark"] .word-name { color: hsl(var(--ink)); }
.word-button[data-tone="warm"] .word-name { color: hsl(30 50% 25%); }
.word-button[data-tone="neutral"] .word-name { color: hsl(var(--ink-soft)); }

/* dc-Remix #14: friction hints — surface NEIGHBOR_FRICTION data as a
   discovery tool on the B column once Word A is picked.
   - .friction-target: thin red border + 'Creates friction.' italic aside
   - .friction-too-close: 70% opacity + 'Too close.' italic aside
   Nothing is blocked. Pure visual suggestion. Both decorations live as
   ::after pseudo-content so no JS DOM mutation. */
.word-button.friction-target {
  border: 1px solid hsl(var(--accent) / 0.55);
  box-shadow: 0 0 0 1px hsl(var(--accent) / 0.18) inset;
  position: relative;
}
.word-button.friction-target::after {
  content: "Creates friction.";
  display: block;
  font-family: var(--serif);
  font-style: italic;
  font-size: 0.72rem;
  color: hsl(var(--accent));
  margin-top: 0.35rem;
  letter-spacing: 0.01em;
}
.word-button.friction-too-close {
  opacity: 0.62;
  filter: saturate(0.8);
  position: relative;
}
.word-button.friction-too-close::after {
  /* UI Notes #2 (May 2026): copy nudge from 'Too close.' to 'Too harmonious.' */
  content: "Too harmonious.";
  display: block;
  font-family: var(--serif);
  font-style: italic;
  font-size: 0.72rem;
  color: hsl(var(--ink-faint));
  margin-top: 0.35rem;
  letter-spacing: 0.01em;
}
/* When a friction-target or too-close button is selected, the aside hides
   so the visual conflict (selected + hint) doesn't muddle the chrome. */
.word-button.selected.friction-target::after,
.word-button.selected.friction-too-close::after {
  display: none;
}
.word-button.selected.friction-too-close {
  opacity: 1;
  filter: none;
}

.picker-summary {
  margin: 2.5rem auto 1rem;
  text-align: center;
  max-width: 600px;
}

#picker-summary-text {
  font-family: var(--serif);
  font-size: 1.1rem;
  font-style: italic;
  color: hsl(var(--ink-soft));
  margin-bottom: 1.25rem;
  /* UI Notes #3 (May 2026): 200ms crossfade when the summary text changes,
     so 'Choose your words' → 'A village of X and Y. Begin?' feels like a
     reveal instead of a swap. The transition only animates when the
     opacity property is touched, which is fine because the JS just changes
     textContent (opacity stays at 1). The smooth feel comes from the
     parchment pulse below firing the moment both words land. */
  transition: opacity 0.2s ease;
}

/* UI Notes #3 (May 2026): parchment-seal pulse on #begin-button when the
   button transitions from disabled to enabled (both words selected). One
   shot of an accent-tinted box-shadow expanding outward, then settling.
   Pure CSS; fires from the change in :not(:disabled) selector match. */
#begin-button:not(:disabled) {
  animation: summary-seal-pulse 0.7s ease-out 1;
}
@keyframes summary-seal-pulse {
  0%   { box-shadow: 0 0 0 0 hsl(var(--accent) / 0.0); }
  35%  { box-shadow: 0 0 0 8px hsl(var(--accent) / 0.18); }
  100% { box-shadow: 0 0 0 0 hsl(var(--accent) / 0.0); }
}
@media (prefers-reduced-motion: reduce) {
  #begin-button:not(:disabled) { animation: none; }
  #picker-summary-text { transition: none; }
}

.word-inline {
  font-family: var(--display);
  font-style: normal;
  color: hsl(var(--accent));
  letter-spacing: 0.01em;
}

/* Nameplate buttons — "Set the seal", "Let the day pass", "Start a new chronicle".
   PR #44 (audit microcopy + chrome pass) reworks the prior dark-fill nameplate
   to read as a drawn slot on parchment, not a filled UI rectangle (brief F).
   The button is ink on parchment with a hairline inset border + ruled lines
   above and below — the typographic register of a drawn mark, not a chip.
   IM Fell English serif type, small-caps mixed-case (not upper-case which
   the prior version used). The result reads as continuous with chronicle
   prose rather than as a web button. */
.primary-button {
  font-family: var(--display);
  font-size: 1rem;
  font-weight: 400;
  font-style: normal;
  font-variant: small-caps;
  letter-spacing: 0.08em;
  text-transform: none;
  background: transparent;
  color: hsl(var(--ink));
  border: none;
  border-top: 1px solid hsl(var(--ink) / 0.55);
  border-bottom: 1px solid hsl(var(--ink) / 0.55);
  border-radius: 0;
  padding: 0.8rem 1.8rem;
  cursor: pointer;
  transition: color 0.18s ease, letter-spacing 0.18s ease, border-color 0.18s ease;
  position: relative;
  box-shadow: none;
}

.primary-button:hover {
  color: hsl(var(--accent));
  letter-spacing: 0.12em;
  border-top-color: hsl(var(--accent) / 0.85);
  border-bottom-color: hsl(var(--accent) / 0.85);
  box-shadow: none;
}

.primary-button:focus-visible {
  outline: 2px solid hsl(var(--accent));
  outline-offset: 4px;
}

/* On phones, the drawn-slot type tightens but keeps its register. */
@media (max-width: 640px) {
  .primary-button {
    font-size: 0.92rem;
    letter-spacing: 0.06em;
    padding: 0.7rem 1.1rem;
  }
  .primary-button:hover { letter-spacing: 0.08em; }
}

.primary-button:disabled {
  opacity: 0.3;
  cursor: not-allowed;
  pointer-events: none;
}

.text-button {
  font-family: var(--serif);
  font-style: italic;
  background: transparent;
  border: none;
  color: hsl(var(--accent));
  font-size: 0.95rem;
  cursor: pointer;
  text-decoration: underline;
  text-decoration-color: hsl(var(--accent) / 0.4);
  text-underline-offset: 4px;
}
.text-button:hover {
  text-decoration-color: hsl(var(--accent));
}

.picker-footer {
  margin-top: 3.5rem;
  text-align: center;
  font-family: var(--serif);
  font-style: italic;
  font-size: 0.9rem;
  color: hsl(var(--ink-faint));
}

/* I-2 (Pioneer Drifter): localStorage-broken advisory.
   Shown only when the storage probe fails (private/incognito modes).
   Smaller and quieter than the chronicler footer line — informational,
   not alarming. */
.picker-storage-advisory {
  margin-top: 0.6rem;
  font-family: var(--serif);
  font-style: italic;
  font-size: 0.82rem;
  color: hsl(var(--ink-faint));
  opacity: 0.8;
}

/* ─────────────────────────────────────────────────────────────────────────
   Village + Log
   ───────────────────────────────────────────────────────────────────────── */

/* IMG_3397 spec: open-book spread on desktop.
   Left page = village + villagers + day actions, right page = log feed,
   thin decorative spine between them. Below 880px collapses to a single
   stacked column with no spine — left page above, right page below. */
.village-frame {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 4px minmax(0, 1.1fr);
  gap: 2rem;
  max-width: 1200px;
  align-items: stretch;
}

.village-side {
  align-self: start;
}

/* Decorative spine — a thin double rule that separates the two pages.
   Pure CSS, no asset. The faint inner red mirrors the chronicle voice. */
.book-spine {
  align-self: stretch;
  background:
    linear-gradient(to bottom,
      transparent 0%,
      hsl(var(--ink) / 0.18) 8%,
      hsl(var(--ink) / 0.18) 92%,
      transparent 100%);
  position: relative;
  width: 1px;
  margin: 0 auto;
}
.book-spine::before,
.book-spine::after {
  content: "";
  position: absolute;
  top: 8%;
  bottom: 8%;
  width: 1px;
  background: inherit;
}
.book-spine::before { left: -3px; }
.book-spine::after  { right: -3px; }

.book-page-left {
  padding-right: 0.5rem;
}
.book-page-right {
  padding-left: 0.5rem;
}

@media (max-width: 880px) {
  .village-frame {
    grid-template-columns: 1fr;
    gap: 1.6rem;
  }
  .book-spine { display: none; }
  .book-page-left, .book-page-right { padding: 0; }
}

/* Right-page log-feed header per IMG_3397 — large display caps. */
.log-feed-header {
  font-family: var(--display);
  font-size: 1.6rem;
  font-weight: 400;
  letter-spacing: 0.06em;
  text-align: center;
  margin: 0 0 1.4rem;
  color: hsl(var(--ink));
  border-bottom: 1px solid hsl(var(--ink) / 0.12);
  padding-bottom: 0.6rem;
}

/* dc-Remix #7: consequential echo — italic accent-red line between header
   and first log entry. Reads state.consequentialEvent.gist; updates via
   0.4s opacity crossfade (refreshConsequentialEcho in ui.js); collapses
   on Day 14 as the Chronicle absorbs it. */
.consequential-echo {
  font-family: var(--serif);
  font-style: italic;
  font-size: 0.95rem;
  color: hsl(var(--accent));
  text-align: center;
  margin: -0.4rem 0 1.2rem;
  padding: 0 1rem;
  line-height: 1.4;
  transition: opacity 0.4s ease;
  opacity: 1;
}
.consequential-echo[data-empty="true"] {
  display: none;
}

/* dc-Remix #7 (mock_06 normative): consequential echo rendered as a
   blockquote with the LETHAL-entry idiom — left red border, accent-tinted
   parchment, no centering. The doc treats the echo as a consequential
   citation. Override the plain .consequential-echo above only when the
   .consequential-echo-quote class is present, so any callers still
   reaching for the plain form keep the old look. */
.consequential-echo-quote {
  font-style: italic;
  text-align: left;
  margin: -0.2rem 0 1.4rem;
  padding: 0.7rem 1rem 0.7rem 1.1rem;
  border: none;
  border-left: 4px solid hsl(var(--accent));
  background: hsl(var(--accent) / 0.08);
  color: hsl(var(--accent));
  line-height: 1.45;
}
.consequential-echo-quote::before {
  /* Subtle opening-quote dingbat that matches the manuscript register. */
  content: "\201C";
  margin-right: 0.15em;
  font-family: var(--display, var(--serif));
  font-size: 1.1em;
  color: hsl(var(--accent) / 0.7);
}
.consequential-echo-quote::after {
  content: "\201D";
  margin-left: 0.15em;
  font-family: var(--display, var(--serif));
  font-size: 1.1em;
  color: hsl(var(--accent) / 0.7);
}

/* dc-Remix #9: ambient decree card — appears once on Day 8 or 9 at the
   bottom of the left page (under the action strip). Pure ceremony —
   the engine never reads the choice. Quiet ink on parchment, three
   options + Pass-without-note skip. */
.decree-card {
  margin-top: 1.4rem;
  padding: 1.1rem 1.2rem 1rem;
  border-top: 1px solid hsl(var(--ink) / 0.18);
  background: hsl(var(--parchment) / 0.45);
  border-radius: 2px;
  transition: opacity 0.35s ease, max-height 0.35s ease;
  max-height: 500px;
  overflow: hidden;
}
.decree-card-header {
  display: flex;
  align-items: baseline;
  gap: 0.5rem;
  margin-bottom: 0.55rem;
}
.decree-day {
  font-family: var(--sans);
  font-size: 0.72rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: hsl(var(--ink-faint));
}
.decree-prompt {
  font-family: var(--serif);
  font-style: italic;
  font-size: 0.95rem;
  color: hsl(var(--ink));
  margin: 0 0 0.8rem;
  line-height: 1.4;
}
.decree-options {
  display: flex;
  flex-direction: column;
  gap: 0.45rem;
}
.decree-option,
.decree-pass {
  font-family: var(--serif);
  font-size: 0.92rem;
  text-align: left;
  background: transparent;
  border: 1px solid hsl(var(--ink) / 0.18);
  padding: 0.55rem 0.85rem;
  cursor: pointer;
  color: hsl(var(--ink));
  line-height: 1.4;
  border-radius: 2px;
  transition: background 0.15s ease, border-color 0.15s ease;
}
.decree-option:hover,
.decree-option:focus-visible {
  background: hsl(var(--accent) / 0.06);
  border-color: hsl(var(--accent) / 0.5);
  outline: none;
}
.decree-pass {
  font-style: italic;
  color: hsl(var(--ink-faint));
  border-style: dashed;
  align-self: flex-end;
  margin-top: 0.35rem;
}
.decree-pass:hover,
.decree-pass:focus-visible {
  color: hsl(var(--ink));
  border-color: hsl(var(--ink) / 0.4);
  outline: none;
}

/* dc-Remix #15: marginal annotation — the player's own hand on a log
   entry. Long-press (touch) or right-click (desktop) opens the editor;
   the saved note renders in the left margin of the entry as a small
   italic IM Fell English aside. The way illuminated manuscripts have
   always had marginal commentary. */
.log-entry {
  position: relative; /* anchor for the absolute log-annotation */
}
.log-annotation {
  position: absolute;
  left: -1.6rem;
  top: 0.4rem;
  width: 1.3rem;
  font-family: var(--display);
  font-style: italic;
  font-size: 0.7rem;
  line-height: 1.15;
  color: hsl(var(--accent) / 0.85);
  text-align: right;
  writing-mode: vertical-rl;
  text-orientation: mixed;
  white-space: nowrap;
  letter-spacing: 0.02em;
}
@media (max-width: 880px) {
  .log-annotation {
    position: static;
    width: auto;
    writing-mode: horizontal-tb;
    text-align: left;
    margin: 0 0 0.4rem;
    font-size: 0.78rem;
    color: hsl(var(--accent));
  }
}
.annotation-editor {
  margin-top: 0.6rem;
  padding: 0.5rem 0.65rem;
  background: hsl(var(--parchment) / 0.6);
  border: 1px solid hsl(var(--accent) / 0.4);
  border-radius: 2px;
}
.annotation-editor-label {
  display: block;
  font-family: var(--sans);
  font-size: 0.72rem;
  letter-spacing: 0.05em;
  color: hsl(var(--ink-faint));
  margin-bottom: 0.4rem;
}
.annotation-editor-counter {
  color: hsl(var(--accent));
  font-weight: 600;
}
.annotation-editor-input {
  width: 100%;
  font-family: var(--display);
  font-style: italic;
  font-size: 0.95rem;
  color: hsl(var(--ink));
  background: transparent;
  border: none;
  border-bottom: 1px solid hsl(var(--ink) / 0.4);
  padding: 0.2rem 0;
  outline: none;
}
.annotation-editor-input:focus {
  border-bottom-color: hsl(var(--accent));
}

/* dc-Remix #9: chronicle margin echo of the decree. Right-floated italic
   accent-red note next to the P5 paragraph — the village's quiet question
   surfacing as marginalia in the final book. */
.chronicle-decree-margin {
  float: right;
  width: 38%;
  max-width: 14rem;
  margin: 0.2rem 0 0.6rem 1.1rem;
  padding: 0.4rem 0.7rem;
  font-family: var(--serif);
  font-style: italic;
  font-size: 0.85rem;
  color: hsl(var(--accent));
  border-left: 1px solid hsl(var(--accent) / 0.4);
  line-height: 1.4;
}
@media (max-width: 640px) {
  .chronicle-decree-margin {
    float: none;
    width: auto;
    max-width: none;
    margin: 0.6rem 0;
    border-left: 2px solid hsl(var(--accent) / 0.5);
  }
}

/* dc-Remix #15: player's annotation surfaces in the Chronicle margin
   alongside P4 if the consequential event got an annotation during the
   run. IM Fell English italic accent-red — the player's own hand on
   the final book. */
.chronicle-player-annotation {
  float: left;
  width: 32%;
  max-width: 12rem;
  margin: 0.2rem 1.1rem 0.6rem 0;
  padding: 0.4rem 0.7rem;
  font-family: var(--display);
  font-style: italic;
  font-size: 0.92rem;
  color: hsl(var(--accent));
  border-right: 1px solid hsl(var(--accent) / 0.4);
  line-height: 1.35;
  text-align: right;
}
@media (max-width: 640px) {
  .chronicle-player-annotation {
    float: none;
    width: auto;
    max-width: none;
    margin: 0.6rem 0;
    border-right: none;
    border-left: 2px solid hsl(var(--accent) / 0.5);
    text-align: left;
    padding-left: 0.7rem;
  }
}

/* Hand-drawn ink frame around the village illustration per IMG_3397.
   Corner ornaments are pure CSS pseudo-shaped spans — a small angle bracket
   in each corner of the illustration container. */
.framed-illustration {
  position: relative;
  border: 1px solid hsl(var(--ink) / 0.45);
  border-radius: 2px;
  padding: 1rem 0.6rem 0.4rem;
  background: hsl(var(--parchment) / 0.4);
  box-shadow: inset 0 0 0 1px hsl(var(--parchment) / 0.6),
              inset 0 0 12px hsl(var(--parchment-edge) / 0.25);
}
.frame-corner {
  position: absolute;
  width: 14px;
  height: 14px;
  pointer-events: none;
  border-color: hsl(var(--ink) / 0.65);
  border-style: solid;
  border-width: 0;
}
.frame-corner-tl { top: -1px; left: -1px;  border-top-width: 2px; border-left-width: 2px; }
.frame-corner-tr { top: -1px; right: -1px; border-top-width: 2px; border-right-width: 2px; }
.frame-corner-bl { bottom: -1px; left: -1px;  border-bottom-width: 2px; border-left-width: 2px; }
.frame-corner-br { bottom: -1px; right: -1px; border-bottom-width: 2px; border-right-width: 2px; }

/* K-1b council K-1 finding F1: .village-tokens row and its .v-token /
   .v-token-more children removed. The single-letter-sigil-initial row
   was redundant with the labeled <ul class="villagers-alive"> roster
   directly below. See game/ui.js renderVillagers() and the deleted
   renderVillagerTokens function for context. */

/* dc-Remix #3 (M-1b): action strip is a COLUMN — the bookmark ribbon
   spans the full width as the top row; the "Let the day pass" button
   sits beneath it. The doc literally says "Add a 14-segment progress
   track above the 'Let the day pass' button." */
.village-day-actions {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: 1rem;
  margin-top: 1.4rem;
  padding-top: 1rem;
  border-top: 1px solid hsl(var(--ink) / 0.15);
}
.village-day-actions > #next-day {
  align-self: stretch;
}
.day-label {
  font-family: var(--display);
  font-size: 1.6rem;
  color: hsl(var(--ink));
  line-height: 1;
}
@media (max-width: 480px) {
  .day-label { font-size: 1.3rem; }
}

/* dc-Remix #3: "Day N of fourteen" ribbon block + 14-segment progress
   track. Reads as a bookmark ribbon, not a health bar — the player feels
   the Chronicle approaching from Day 10 onward. Layout: dual-line label
   on top, full-width track row beneath (Day 1 ... segments ... Day 14
   ↗ Chronicle), the whole ribbon sitting above the "Let the day pass"
   button. */
.day-ribbon {
  display: flex;
  flex-direction: column;
  gap: 0.55rem;
  align-items: stretch;
}
.day-ribbon-label {
  display: flex;
  align-items: baseline;
  gap: 0.5rem;
}
.day-label-sub {
  font-family: var(--sans);
  font-size: 0.72rem;
  letter-spacing: 0.12em;
  text-transform: lowercase;
  color: hsl(var(--ink-faint));
  font-style: italic;
}
/* M-1b: progress row spans the full width with end labels at the ribbon's
   ends so the player reads Day 1 (start) → segments → Day 14 Chronicle
   (end) like a bookmark. */
.day-progress-row {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  width: 100%;
}
.day-progress-end {
  font-family: var(--sans);
  font-size: 0.66rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: hsl(var(--ink-faint));
  white-space: nowrap;
  flex-shrink: 0;
}
.day-progress-end-final {
  color: hsl(var(--accent) / 0.85);
  letter-spacing: 0.06em;
}
.day-progress-track {
  display: flex;
  gap: 4px;
  align-items: center;
  flex: 1 1 auto;
  justify-content: space-between;
  min-width: 0;
}
.day-segment {
  display: inline-block;
  width: 10px;
  height: 2px;
  background: hsl(var(--ink) / 0.18);
  border-radius: 1px;
  transition: background 0.3s ease, transform 0.3s ease;
}
.day-segment-filled {
  background: hsl(var(--accent) / 0.7);
}
.day-segment-current {
  width: 12px;
  height: 4px;
  background: hsl(var(--accent));
  border-radius: 2px;
  box-shadow: 0 0 0 1px hsl(var(--accent) / 0.25);
}
@media (max-width: 480px) {
  .day-segment { width: 8px; }
  .day-segment-current { width: 10px; }
  .day-progress-track { gap: 3px; }
}

/* Log entries with the .entry-ribbon class (most-recent + meeting outcomes)
   get a red ribbon corner tab at the top-left, per IMG_3397. Quieter entries
   keep the existing thin red rule on the left edge. */
.log-entry.entry-ribbon {
  position: relative;
}
.log-entry.entry-ribbon::before {
  content: "";
  position: absolute;
  top: -2px;
  left: -2px;
  width: 14px;
  height: 22px;
  background: hsl(var(--accent));
  /* Notch the bottom into a ribbon point */
  clip-path: polygon(0 0, 100% 0, 100% 100%, 50% 78%, 0 100%);
  box-shadow: 0 1px 3px hsl(0 0% 0% / 0.25);
  pointer-events: none;
}

.words-display {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.75rem;
  margin-bottom: 1.5rem;
}

.word-tag {
  font-family: var(--display);
  font-size: 1.4rem;
  padding: 0.3rem 0.85rem;
  border: 1px solid hsl(var(--ink) / 0.4);
  border-radius: 2px;
  background: hsl(var(--parchment-deep));
}

.word-tag[data-tone="dark"] { color: hsl(var(--ink)); border-color: hsl(var(--ink) / 0.6); }
.word-tag[data-tone="warm"] { color: hsl(30 50% 28%); border-color: hsl(30 35% 50%); }
.word-tag[data-tone="neutral"] { color: hsl(var(--ink-soft)); }

.word-amp {
  font-family: var(--serif);
  font-style: italic;
  color: hsl(var(--ink-faint));
}

.village-illustration {
  border: 1px solid hsl(var(--ink) / 0.2);
  background: hsl(var(--parchment-deep));
  padding: 0.75rem;
  margin-bottom: 1.25rem;
  border-radius: 2px;
}

/* K-1c council K-1, finding F3-B-Opus: post-meal village tone.
   When state.meal.chosenId is set on the engine (and the meal wasn't
   a ghost run — see game/ui.js renderVillageScreen for the gate),
   the village-illustration wrapper carries data-village-tone="post-meal".
   The CSS layers a SUBTLE luminance shift on the watercolor image
   underneath: ~4% brightness drop + ~4% sepia warm. NO chromatic
   shift, NO opacity change, NO animation. The shift is below the
   threshold of 'Instagram filter' — the player should feel 'later
   in the same season,' not 'different image.'

   Falsifiable test from the K-1 decision §F3-B-Opus: a screenshot
   diff at Day 1 vs Day 8 of the same word pair must read as 'later'
   to a fresh viewer, NOT as 'different image.' Verified via
   tools/beta_reports/K1c_*.png screenshots in the implementation PR.

   The shift NEVER applies to the meeting or chronicle scenes (they
   have their own typographic register — condition 3.d) because the
   selector is scoped to .village-illustration specifically; .meeting-
   illustration and .chronicle-illustration are different DOM nodes. */
.village-illustration[data-village-tone="post-meal"] .scene-image {
  filter: brightness(0.96) sepia(0.04);
}

/* Some users find chromatic shifts disorienting even when not animated;
   prefers-reduced-motion turns the tone off entirely (Opus condition 9.e). */
@media (prefers-reduced-motion: reduce) {
  .village-illustration[data-village-tone="post-meal"] .scene-image {
    filter: none;
  }
}

.village-svg {
  width: 100%;
  height: auto;
  color: hsl(var(--ink));
  display: block;
}

.villagers-list {
  font-family: var(--serif);
  font-size: 0.92rem;
}

.villagers-heading {
  font-family: var(--sans);
  font-size: 0.7rem;
  font-weight: 500;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: hsl(var(--ink-faint));
  margin: 1.25rem 0 0.5rem;
}

.villagers-heading.dim { color: hsl(var(--accent) / 0.7); }

.villagers-alive, .villagers-dead {
  list-style: none;
}

/* dc-Remix #5: grudge connector overlay sits absolutely positioned inside
   the alive list, drawn after mount/refresh by drawGrudgeConnectors. The
   dots themselves are inline-flex children at the far-right of each row. */
.villagers-alive {
  position: relative;
}
.v-grudge-dot {
  margin-left: auto;
  width: 7px;
  height: 7px;
  min-width: 7px;
  min-height: 7px;
  flex: 0 0 7px;
  border-radius: 50%;
  background: hsl(var(--accent));
  align-self: center;
  display: block;
}
.grudge-connectors {
  position: absolute;
  inset: 0;
  pointer-events: none;
  overflow: visible;
}
.grudge-line {
  /* UI Notes #9 (M-9): the thread connecting a grudge-holder to its target.
     Was 1px @ 60% alpha which got lost in the parchment grain at small
     viewport widths. Bumped to 1.5px and higher alpha, plus a faint dashed
     pattern so it reads as a deliberate red thread rather than a render
     artifact. The doc mock_08 shows it as a thin but clearly visible line. */
  stroke: hsl(var(--accent) / 0.85);
  stroke-width: 1.5;
  stroke-linecap: round;
}

/* UI Notes #9 (M-9): sigil-object subtitle beneath each villager name.
   Doc mock_08 shows 'the tooth', 'the candle', 'the open hand' as italic
   light-ink subtitles directly beneath each name line. Reads as the
   chronicler's shorthand. */
.v-sigil-subtitle {
  display: block;
  margin-top: 0.05rem;
  font-family: var(--serif);
  font-style: italic;
  font-size: 0.78rem;
  color: hsl(var(--ink) / 0.55);
  letter-spacing: 0.01em;
}

.villagers-alive li, .villagers-dead li {
  display: flex;
  align-items: center;
  gap: 0.55rem;
  padding: 0.32rem 0;
  border-bottom: 1px solid hsl(var(--ink) / 0.07);
}

/* Inline 40px portrait — ink-line composed from portraits.js. Stays under-rendered
   relative to the Day 14 standout (168px) so the chronicler prose remains primary. */
.v-portrait {
  flex-shrink: 0;
  width: 40px;
  height: 52px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  color: hsl(var(--ink) / 0.92);
}

.v-portrait svg {
  width: 40px;
  height: 52px;
  display: block;
}

.villagers-dead .v-portrait {
  filter: grayscale(1) opacity(0.55);
}

/* dc-Remix #4: "A death deserves a ceremony." 500ms red glow radiating
   from the portrait border, then dissipates as the filter settles to
   greyscale. The keyframes override the static .villagers-dead filter
   for the duration of the animation; the static rule takes back over
   at animationend (also when JS removes the data-just-died attribute).
   Edward Gorey's panels have drama in the line weight, not the caption —
   the prose stays flat, the visual carries the weight. */
@keyframes ink-stain {
  0% {
    box-shadow: 0 0 0 0 hsl(var(--accent) / 0), 0 0 0 0 hsl(var(--accent) / 0);
    filter: grayscale(0) opacity(1);
  }
  18% {
    box-shadow: 0 0 14px 6px hsl(var(--accent) / 0.55), 0 0 4px 1px hsl(var(--accent) / 0.85);
    filter: grayscale(0.1) opacity(1);
  }
  100% {
    box-shadow: 0 0 0 0 hsl(var(--accent) / 0), 0 0 0 0 hsl(var(--accent) / 0);
    filter: grayscale(1) opacity(0.55);
  }
}
.v-portrait[data-just-died="true"] {
  animation: ink-stain 500ms ease-out forwards;
  border-radius: 4px; /* gives the box-shadow a clean rounded contour */
}
@media (prefers-reduced-motion: reduce) {
  /* Skip the glow; jump straight to the resolved greyscale. The strikethrough
     and grey filter still carry the death; the ceremony is the optional layer. */
  .v-portrait[data-just-died="true"] {
    animation: none;
    filter: grayscale(1) opacity(0.55);
  }
}

.v-text {
  flex: 1 1 auto;
  min-width: 0;
}

.villagers-dead li {
  /* dc-Remix #4: keep the strike on the name itself, NOT the death
     annotation sub-row. Without scoping, '† Day 8 · the well' got struck
     through too, which hid the very story the annotation is meant to tell.
     Apply the line-through to .v-name-line only; let the annotation breathe. */
  color: hsl(var(--ink-faint));
}
.villagers-dead .v-name-line {
  text-decoration: line-through;
  text-decoration-color: hsl(var(--accent) / 0.4);
}

/* dc-Remix #4: blood-red italic death annotation sub-row, mock_03 normative. */
.v-death-annotation {
  display: block;
  margin-top: 0.15rem;
  font-family: var(--font-body, serif);
  font-style: italic;
  font-size: 0.82em;
  color: hsl(var(--accent));
  letter-spacing: 0.01em;
}

/* dc-Remix #1: the full 'Name of the Sigil' rendered in the villager's hand.
   .villager-hand sets the font-family via the per-key rule (hand-caveat,
   hand-blackletter, hand-tangerine, hand-reenie at styles.css:4081+).
   We add a small relative font-size bump so the script reads at the same
   optical size as the surrounding UI serif. */
.v-name-line {
  display: block;
  line-height: 1.25;
}
.v-name-line.villager-hand .v-sigil {
  /* Inside the hand wrap, the sigil epithet shares the same hand font.
     Override the italic-only treatment so the whole string reads as one
     handwritten unit per the doc mock. */
  font-style: normal;
  color: inherit;
}

.v-name {
  font-weight: 500;
  color: hsl(var(--ink));
}

.villagers-dead .v-name {
  color: hsl(var(--ink-faint));
}

.v-sigil {
  font-style: italic;
  color: hsl(var(--ink-faint));
}

/* Per-villager glyph strip — one ink mark per accumulated counter.
   Sits at the right edge so the row reads name → sigil → history. */
.v-glyphs {
  margin-left: auto;
  display: inline-flex;
  gap: 0.3rem;
  align-items: center;
  flex-shrink: 0;
}

.v-glyph {
  color: hsl(var(--ink) / 0.78);
  vertical-align: middle;
  flex-shrink: 0;
}

.villagers-dead .v-glyph {
  color: hsl(var(--ink-faint) / 0.7);
}

/* .day-header / .day-title were the right-side day header in the old
   layout. PR #9 moved the day label + "Let the day pass" button to the bottom
   of the left page (.village-day-actions / .day-label) per IMG_3397.
   No element references these classes anymore. Removed at the root. */

.log-feed {
  display: flex;
  flex-direction: column;
  gap: 1.5rem;
}

.log-entry {
  font-family: var(--serif);
  font-size: 1.05rem;
  line-height: 1.65;
  color: hsl(var(--ink-soft));
  padding: 1.25rem 1.5rem;
  background: hsl(var(--parchment-deep) / 0.55);
  border-left: 2px solid hsl(var(--ink) / 0.3);
  border-radius: 0 2px 2px 0;
  transition: opacity 0.6s ease, transform 0.6s ease;
}

.log-entry[data-kind="MEETING_ARRIVAL"],
.log-entry[data-kind="MEETING_OUTCOME"] {
  border-left-color: hsl(var(--accent));
  background: hsl(var(--accent) / 0.05);
}

.log-entry[data-kind="MEETING_AFTERMATH"] {
  border-left-color: hsl(var(--accent) / 0.5);
  background: hsl(var(--accent) / 0.03);
}

.log-entry[data-kind="T16"] {
  border-left-color: hsl(var(--ink) / 0.15);
  background: transparent;
  color: hsl(var(--ink-faint));
  font-style: italic;
}

/* dc-Remix #2: LETHAL / QUIET log entry differentiation. The Gorey thesis
   says "flat tone whether reporting horror or beauty" — that applies to
   prose. The visual register should carry what the prose doesn't say.
   LETHAL entries get heavier ink; QUIET entries recede. RIPENING is the
   default register, unchanged. The bucket comes from TEMPLATE_META in
   phrasebook.js and reaches the DOM via the engine push site. */
.log-entry[data-bucket="LETHAL"] {
  border-left-width: 4px;
  border-left-color: hsl(var(--accent));
  background: hsl(var(--accent) / 0.08);
  font-size: 1.09rem; /* +0.04rem per the doc */
}
.log-entry[data-bucket="LETHAL"] .log-day::before {
  content: "\2726\00a0"; /* ✦ (heavy four pointed black star) + nbsp */
  color: hsl(var(--accent));
  font-size: 0.85rem;
}

/* dc-Remix #2 (header line): manuscript-citation pieces inside .log-day.
   The doc mock_01 shows 'DAY 4 · T01 WELL INCIDENT · LETHAL' all on one
   row, in spaced caps, with LETHAL in blood-red. The citation must NOT
   wrap unless the viewport is genuinely narrow; on mobile it can break
   to a second line below the day label. */
.log-day {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.45rem;
  row-gap: 0.1rem;
}
.log-day-sep,
.log-entry-citation-sep {
  color: hsl(var(--ink) / 0.35);
  font-weight: 400;
}
.log-entry-citation {
  font-size: 0.75em;
  font-weight: 500;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: hsl(var(--ink) / 0.55);
}
.log-entry-tag {
  font-size: 0.72em;
  font-weight: 600;
  letter-spacing: 0.12em;
  text-transform: uppercase;
}
.log-entry-tag-lethal {
  color: hsl(var(--accent));
}
/* When the lethal entry already prepends a ✦ via .log-day::before,
   tighten the gap so the row reads as one citation, not two stacked. */
.log-entry[data-bucket="LETHAL"] .log-day .log-day-label {
  display: inline;
}
/* QUIET bucket applies to any quiet entry, not only T16 (e.g. authored
   QUIET-bucket templates T22/T23/T24/T29 in TEMPLATE_META). The existing
   T16 rule above already covers the legacy QUIET T16 case; this matches
   anything tagged QUIET so the new ESTABLISHING and reverberation pool
   entries also recede. */
.log-entry[data-bucket="QUIET"]:not([data-kind="T16"]) {
  border-left-color: hsl(var(--ink) / 0.15);
  background: transparent;
  color: hsl(var(--ink-faint));
  font-style: italic;
}

.log-day {
  font-family: var(--sans);
  font-size: 0.7rem;
  font-weight: 500;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: hsl(var(--ink-faint));
  margin-bottom: 0.6rem;
}

.log-text p {
  margin-bottom: 0.7rem;
}
.log-text p:last-child {
  margin-bottom: 0;
}

.entry-fresh {
  animation: fade-in 0.7s ease both;
}

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

/* ─────────────────────────────────────────────────────────────────────────
   Page-level ornaments — shared by meeting (IMG_3398) and chronicle (IMG_3399)
   ◦ page-corner = vine flourish (meeting) or angle bracket (chronicle)
   ◦ page-strip  = label + horizontal rule with diamond at center
   ───────────────────────────────────────────────────────────────────────── */

.page-frame.ornamented {
  position: relative;
}

/* Four corners of the parchment frame. The meeting variant uses currentColor
   at faded ink; the chronicle variant the same. Top-right and bottom-left
   are mirrored via transform so the same SVG works in every corner. */
.page-corner {
  position: absolute;
  width: 28px;
  height: 28px;
  color: hsl(var(--ink) / 0.55);
  pointer-events: none;
}
@media (max-width: 640px) {
  .page-corner { width: 22px; height: 22px; }
}
.page-corner-tl { top: 14px; left: 14px;  transform: none; }
.page-corner-tr { top: 14px; right: 14px; transform: scaleX(-1); }
.page-corner-bl { bottom: 14px; left: 14px;  transform: scaleY(-1); }
.page-corner-br { bottom: 14px; right: 14px; transform: scale(-1, -1); }

/* Header strip at the top of the meeting and chronicle pages. The diamond
   is a CSS-rotated square positioned absolutely at the center of the rule. */
.page-strip {
  text-align: center;
  margin: 0.4rem 0 1.6rem;
}
.page-strip-label {
  font-family: var(--sans);
  text-transform: uppercase;
  letter-spacing: 0.22em;
  font-size: 0.74rem;
  color: hsl(var(--ink-faint));
  display: block;
  margin-bottom: 0.5rem;
}
.page-strip-rule {
  display: block;
  position: relative;
  height: 1px;
  margin: 0 auto;
  width: min(380px, 70%);
  background: hsl(var(--ink) / 0.22);
}
.page-strip-diamond {
  position: absolute;
  top: 50%;
  left: 50%;
  width: 6px;
  height: 6px;
  background: hsl(var(--ink) / 0.55);
  transform: translate(-50%, -50%) rotate(45deg);
}

/* ─────────────────────────────────────────────────────────────────────────
   Day 7 Meeting
   ───────────────────────────────────────────────────────────────────────── */

.meeting-frame {
  max-width: 760px;
  margin: 0 auto;
}

.meeting-header {
  text-align: center;
  margin-bottom: 2rem;
}

/* The .torn-prose modifier (added in PR #10) replaces the rectangular box
   with red-left-rule with a torn-edge parchment box per IMG_3398. The torn
   look comes from clip-path with irregular polygon points + a slightly
   lighter inner parchment fill for visible separation from the page. */
.meeting-arrival {
  font-family: var(--serif);
  font-size: 1.1rem;
  line-height: 1.7;
  color: hsl(var(--ink-soft));
  padding: 1.7rem 1.9rem;
  background: hsl(var(--parchment-deep));
  border: 1px solid hsl(var(--ink) / 0.18);
  border-left: 3px solid hsl(var(--accent));
  border-radius: 2px;
  margin-bottom: 2rem;
}
.meeting-arrival.torn-prose {
  background:
    radial-gradient(ellipse at 12% 18%, hsl(38 38% 92% / 0.5) 0%, transparent 45%),
    radial-gradient(ellipse at 88% 82%, hsl(36 32% 84% / 0.45) 0%, transparent 50%),
    hsl(38 36% 88%);
  border: 0;
  border-left: 0;
  padding: 1.9rem 2.1rem;
  /* Irregular polygon edges suggest a hand-torn parchment fragment.
     Stays close to a rectangle so the prose still reads as a block. */
  clip-path: polygon(
    1.5% 0%, 4% 1%, 9% 0.4%, 18% 0.8%, 31% 0.2%, 47% 0.7%, 62% 0.1%, 78% 0.6%, 91% 0.2%, 96% 0.5%, 100% 1.5%,
    99.4% 14%, 100% 28%, 99.6% 42%, 99.8% 60%, 99.4% 76%, 99.7% 92%, 98.5% 99%,
    93% 99.4%, 79% 99.8%, 62% 99.4%, 47% 99.9%, 32% 99.6%, 18% 99.8%, 9% 99.5%, 4% 99.9%, 1% 99%,
    0.4% 86%, 0% 70%, 0.4% 54%, 0% 38%, 0.5% 22%, 0% 10%
  );
  filter: drop-shadow(0 2px 6px hsl(0 0% 0% / 0.18));
}

.meeting-arrival p {
  margin-bottom: 0.85rem;
}

.meeting-arrival p:last-child {
  margin-bottom: 0;
}

.meeting-reveal {
  text-align: center;
  /* Sits inside .meeting-choices between the prompt and the choice grid
     (PR #15), so the spacing is tighter than when it was a sibling block. */
  margin: -0.4rem 0 1.25rem;
}

.revealed-words {
  font-family: var(--serif);
  font-style: italic;
  color: hsl(var(--ink-soft));
  margin-top: 0.5rem;
}

/* L-1 council: neighbor-word taglines under the Day-10 reveal block. */
.revealed-words-taglines {
  font-family: var(--sans);
  font-style: normal;
  font-size: 0.82rem;
  letter-spacing: 0.01em;
  color: hsl(var(--ink-soft));
  margin: 0.4rem 0 0;
  opacity: 0.85;
}
.revealed-words-taglines .tagline + .tagline::before {
  content: " · ";
  opacity: 0.55;
}

.meeting-choices {
  margin-top: 2rem;
}

.prompt {
  font-family: var(--display);
  font-size: 1.4rem;
  text-align: center;
  margin-bottom: 1.25rem;
}

.choice-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 0.85rem;
}

@media (max-width: 640px) {
  .choice-grid { grid-template-columns: 1fr; }
}

/* Per IMG_3398, each choice card carries a small red ribbon corner tab
   at its top-left. Same clip-path shape as the log-entry ribbon, scaled
   for the larger choice card. */
.choice-button {
  position: relative;
  font-family: var(--serif);
  background: transparent;
  border: 1px solid hsl(var(--ink) / 0.3);
  padding: 1.4rem 1rem;
  cursor: pointer;
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
  text-align: center;
  border-radius: 2px;
  transition: all 0.18s ease;
}

.choice-button::before {
  content: "";
  position: absolute;
  top: -3px;
  left: -3px;
  width: 16px;
  height: 26px;
  background: hsl(var(--accent));
  clip-path: polygon(0 0, 100% 0, 100% 100%, 50% 78%, 0 100%);
  box-shadow: 0 1px 3px hsl(0 0% 0% / 0.25);
  pointer-events: none;
  transition: transform 0.18s ease;
}
.choice-button:hover:not(:disabled)::before { transform: translateY(-1px); }

.choice-button:hover:not(:disabled) {
  border-color: hsl(var(--accent));
  background: hsl(var(--accent) / 0.05);
  transform: translateY(-2px);
}

/* UI Notes #13 (May 2026): differentiated ribbon tabs by choice.
   Welcome → warm amber ribbon (same tone family as the meal screen).
   Test    → neutral ink ribbon (no special color; stays the default,
             made slightly muted to reinforce neutrality).
   Refuse  → full accent-red ribbon plus a red card border so the
             posture reads as a refusal before the player reads the label.
   The hover state for all three still uses the accent-red border / tint
   so the focused option always reads as 'the village considers this'. */
.choice-button[data-choice="welcome"]::before {
  background: hsl(36 55% 45%); /* warm amber */
}
.choice-button[data-choice="test"]::before {
  background: hsl(28 22% 28%); /* ink-brown, neutral */
}
.choice-button[data-choice="refuse"]::before {
  background: hsl(var(--accent)); /* accent red, matches existing */
}
.choice-button[data-choice="refuse"] {
  border-color: hsl(var(--accent) / 0.45);
}

.choice-button.chosen {
  border-color: hsl(var(--accent));
  background: hsl(var(--accent) / 0.12);
}

.choice-button:disabled {
  opacity: 0.4;
  cursor: not-allowed;
}

.choice-label {
  font-family: var(--display);
  font-size: 1.5rem;
  color: hsl(var(--ink));
}

.choice-flavor {
  font-family: var(--serif);
  font-style: italic;
  font-size: 0.85rem;
  color: hsl(var(--ink-faint));
  line-height: 1.4;
}

/* dc-Remix #8: meeting choice whispers. A short italic accent-red line
   under the existing .choice-flavor, in the chronicler's register. Hidden
   at rest; revealed on hover or keyboard focus. Not a mechanical spoiler
   — a whisper from the village's mood. */
.choice-whisper {
  display: block;
  font-family: var(--serif);
  font-style: italic;
  font-size: 0.82rem;
  color: hsl(var(--accent));
  line-height: 1.4;
  margin-top: 0.45rem;
  max-height: 0;
  opacity: 0;
  overflow: hidden;
  transition: max-height 0.35s ease 0.15s, opacity 0.3s ease 0.15s, margin-top 0.3s ease 0.15s;
}
.choice-button:hover:not(:disabled) .choice-whisper,
.choice-button:focus-visible:not(:disabled) .choice-whisper {
  max-height: 60px;
  opacity: 1;
  margin-top: 0.45rem;
}
@media (prefers-reduced-motion: reduce) {
  .choice-whisper { transition: none; }
}

.caveat {
  font-family: var(--serif);
  font-style: italic;
  text-align: center;
  margin-top: 1.25rem;
  font-size: 0.85rem;
  color: hsl(var(--accent));
}

/* ─────────────────────────────────────────────────────────────────────────
   Day 14 Chronicle
   ───────────────────────────────────────────────────────────────────────── */

.chronicle-frame {
  max-width: 720px;
  margin: 0 auto;
}

/* Pre-PR-#10, .chronicle-page was the only "page" element so it carried
   its own border + shadow + parchment surface. Now that PR #7 made the
   .page-frame the parchment sheet primitive, .chronicle-page is just an
   inner container. The .chronicle-page-bare modifier (added in PR #10)
   strips the now-redundant box-styling. The base .chronicle-page rule
   stays for backwards compatibility but the bare modifier overrides it. */
/* P8 (Phase 3) — merged the second .chronicle-page block (the unroll
   animation, previously at line ~1120) into this base rule so any future
   edit to chronicle styling has a single block to update. */
.chronicle-page {
  background: hsl(var(--parchment));
  padding: 3rem 3.25rem 2.5rem;
  border: 1px solid hsl(var(--ink) / 0.25);
  border-radius: 2px;
  position: relative;
  box-shadow:
    inset 0 0 60px hsl(35 30% 70% / 0.4),
    0 14px 40px hsl(var(--ink) / 0.18);
  background-image:
    radial-gradient(at 20% 20%, hsl(35 30% 80% / 0.6) 0%, transparent 60%),
    radial-gradient(at 80% 80%, hsl(35 25% 70% / 0.6) 0%, transparent 60%);
  /* Day 14 reveal animation — chronicle unrolls into view */
  animation: unroll 1.4s cubic-bezier(0.23, 1, 0.32, 1) both;
  transform-origin: top center;
}
.chronicle-page.chronicle-page-bare {
  background: transparent;
  background-image: none;
  border: 0;
  border-radius: 0;
  box-shadow: none;
  padding: 1rem 1.5rem 0.5rem;
}

@media (max-width: 640px) {
  .chronicle-page { padding: 2rem 1.5rem; }
  .chronicle-page.chronicle-page-bare { padding: 0.5rem 0.4rem 0.2rem; }
}

.chronicle-header {
  text-align: center;
  margin-bottom: 1.5rem;
}

.chronicle-title {
  font-family: var(--display);
  font-size: 2.1rem;
  font-weight: 400;
  letter-spacing: 0.01em;
  line-height: 1.2;
}

/* IMG_3399: dramatic two-line title. Constrained max-width forces the
   wrap so the title looks like a manuscript front-page heading even when
   the chronicle name is short. */
.chronicle-title.chronicle-title-large {
  font-size: 3.1rem;
  line-height: 1.05;
  max-width: 14ch;
  margin-left: auto;
  margin-right: auto;
  /* Walkthrough fix (2026-05-10): text-wrap: balance evenly distributes
     line breaks so the title doesn't drop "and" alone on a line. The
     engine also inserts a non-breaking space between "and" and the
     second word so the two travel together regardless of width. */
  text-wrap: balance;
}
@media (max-width: 880px) {
  .chronicle-title.chronicle-title-large { font-size: 2.5rem; }
}
@media (max-width: 480px) {
  .chronicle-title.chronicle-title-large { font-size: 2rem; }
}

/* P8 (Phase 3) — .chronicle-rule deleted: not referenced in any HTML or JS. */

.chronicle-illustration {
  display: flex;
  justify-content: center;
  margin: 1.5rem auto 2rem;
  max-width: 340px;
}

/* S2 (Phase 5) — .chronicle-illustration-hero modifier.
   The audit (cross-game Fix 3): the chronicle illustration was 31% of
   frame width — smaller than the prose paragraph that explained it.
   Pentiment's pattern is image-first, prose-second; the image is the
   chronicle's primary visual element and the screenshot artifact players
   share. The -hero variant widens to 100% of the frame's content area
   and adds a thin manuscript-style ruled border so the watercolor reads
   as an illuminated plate rather than a freestanding image. */
.chronicle-illustration.chronicle-illustration-hero {
  display: block;
  margin: 1.4rem auto 2rem;
  max-width: 100%;
  padding: 0;
  border: 1px solid hsl(var(--ink) / 0.4);
  border-radius: 1px;
  /* Inset shadow gives the parchment a slight pressed-into-page quality
     so the watercolor doesn't float against the body of the chronicle. */
  box-shadow:
    inset 0 0 0 4px hsl(var(--parchment)),
    inset 0 0 0 5px hsl(var(--ink) / 0.18),
    0 1px 3px hsl(var(--ink) / 0.12);
  background: hsl(var(--parchment));
}
.chronicle-illustration.chronicle-illustration-hero .scene-image {
  width: 100%;
  display: block;
  /* Override the global 16:9 aspect-ratio: chronicle WebPs are 1402×1122
     (~5:4). Forcing 16:9 cropped the top of the watercolor; honor the
     natural ratio. */
  aspect-ratio: auto;
  height: auto;
}

/* IMG_3399 shows a smaller line-art house cluster above the prose, not
   the full village strip. The .chronicle-illustration-small modifier
   shrinks the existing village SVG to that scale. Kept for compatibility
   with any caller that hasn't migrated to -hero. */
.chronicle-illustration.chronicle-illustration-small {
  max-width: 220px;
  margin: 1.2rem auto 1.6rem;
}
.chronicle-illustration.chronicle-illustration-small .village-svg {
  height: auto;
}

.chronicle-illustration .village-svg {
  filter: sepia(0.18);
}

.chronicle-body {
  font-family: var(--serif);
  font-size: 1.05rem;
  line-height: 1.78;
  color: hsl(var(--ink));
}

.chronicle-body p {
  margin-bottom: 1.1rem;
  text-indent: 1.4em;
}

.chronicle-body p:first-child {
  text-indent: 0;
}

/* Drop cap on the chronicle's first paragraph. PR #10 consolidated the
   two rules that defined this (the original at 3.2rem + the Grok-era
   override at 3.8rem) into one source of truth at the larger size.

   Walkthrough fix (2026-05-10): the ::first-letter pseudo-element
   doesn't reliably rasterize through html-to-image, so the chronicle
   PNG export was losing the drop cap. The renderer now wraps the first
   character in <span class="dropcap"> as real markup, which always
   rasterizes. The ::first-letter rule is kept for non-JS fallback (e.g.
   if the renderer changes) but the .dropcap span is the source of
   truth at runtime. */
.chronicle-body p:first-child::first-letter,
.chronicle-body .dropcap {
  font-family: var(--display);
  font-size: 3.8rem;
  float: left;
  line-height: 0.78;
  margin: 0.16rem 0.34rem 0 0;
  color: hsl(var(--accent));
}

/* Marginalia portrait — small ink-line drawing of the standout villager,
   floated to the right of the paragraph that first names them.
   PR #4 originally specced a 168px sidebar portrait; that predated the
   Phase 5 watercolor hero scene which is now the chronicle's primary
   visual. A 168px sidebar would compete with the watercolor. The
   marginalia approach (72px, floated into the paragraph) reads as a
   manuscript flourish, doesn't displace the hero, and matches how
   illuminated manuscripts actually treated named figures.

   The float-right anchors at the paragraph's start, so the prose wraps
   around it. Negative top-margin pulls the portrait up so it sits flush
   with the paragraph's top line, the way a manuscript marginalia would. */
.chronicle-marginalia-portrait {
  float: right;
  display: inline-block;
  width: 72px;
  height: 96px; /* 100x130 viewBox aspect ratio = 72 × (130/100) */
  margin: -0.1rem 0 0.4rem 0.9rem;
  shape-outside: margin-box;
  /* Subtle ink-on-parchment shadow so the portrait reads as a drawing
     pressed onto the page rather than a freestanding object. */
  filter: drop-shadow(0 1px 0 hsl(var(--ink) / 0.08));
  /* Override the default text-indent applied to chronicle-body p — a
     floated child shouldn't be indented. */
  text-indent: 0;
}

.chronicle-marginalia-portrait svg {
  display: block;
  width: 100%;
  height: 100%;
}

/* Dead-villager variant: desaturated to ink-soft so the chronicle can
   still mark the standout when they are the heretic kind (drift > 0.8
   villager who died) without the portrait reading as alive. */
.chronicle-marginalia-portrait-dead svg {
  opacity: 0.55;
  filter: grayscale(0.6);
}

/* The paragraph that contains the marginalia portrait drops its
   text-indent so the first line aligns flush with the portrait's left
   edge — otherwise the indent visually conflicts with the floated image
   and the paragraph reads as if it has a hole punched in its first
   line. */
.chronicle-paragraph-with-portrait {
  text-indent: 0 !important;
  /* Clear: both — prevent any prior float from leaving a stub of itself
     above this paragraph. */
  clear: both;
}

/* Mobile collapse: at narrow widths a 72px portrait inside a 320px-ish
   column eats too much horizontal space for the prose to wrap around
   gracefully. Place it above the paragraph instead, centered, smaller. */
@media (max-width: 480px) {
  .chronicle-marginalia-portrait {
    float: none;
    display: block;
    width: 56px;
    height: 73px;
    margin: 0.4rem auto 0.6rem;
  }
}

/* Ornamental divider between body and the closing envoi per IMG_3399.
   Short rule + three diamonds (center one larger) + short rule, all
   horizontally centered with flex. */
.chronicle-divider {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  margin: 2rem auto 0;
  width: min(280px, 70%);
}
.chronicle-divider-rule {
  flex: 1;
  height: 1px;
  background: hsl(var(--ink) / 0.28);
}
.chronicle-divider-diamond {
  width: 5px;
  height: 5px;
  background: hsl(var(--ink) / 0.5);
  transform: rotate(45deg);
}
.chronicle-divider-diamond-center {
  width: 8px;
  height: 8px;
  background: hsl(var(--accent) / 0.72);
}

/* dc-Remix #10: per-ending divider glyphs replace the geometric diamonds.
   The four endings each get their own ornament set (peaceful: em-dash +
   diamond, thriving: ❧ + diamond, decimated: † + ✦, changed: ⟳ + ◇).
   For decimated, the glyphs shift accent-red; for thriving, muted green;
   peaceful and changed stay ink. */
.chronicle-divider-glyph {
  /* The doc's per-ending glyphs († ✦ ❧ ⦿ ⟳) include several characters
   that IM Fell English does not cover; fall back to the system serif and
   then a symbol-capable stack so the glyph renders predictably. Tested
   against macOS Apple Color Emoji, Windows Segoe UI Symbol, Linux Noto. */
  font-family: var(--display), Georgia, "Segoe UI Symbol", "Apple Symbols", serif;
  font-size: 1.15rem;
  line-height: 1;
  color: hsl(var(--ink) / 0.55);
  display: inline-block;
}
.chronicle-divider-glyph-center {
  font-size: 1.35rem;
  color: hsl(var(--accent) / 0.78);
}
.chronicle-divider-decimated .chronicle-divider-glyph,
.chronicle-divider-decimated .chronicle-divider-rule {
  color: hsl(var(--accent));
  background: hsl(var(--accent));
}
.chronicle-divider-decimated .chronicle-divider-glyph-center {
  color: hsl(var(--accent));
}
.chronicle-divider-decimated .chronicle-divider-rule {
  background: hsl(var(--accent) / 0.5);
}
.chronicle-divider-thriving .chronicle-divider-glyph-side {
  color: hsl(135 30% 35% / 0.85);
}
.chronicle-divider-thriving .chronicle-divider-glyph-center {
  color: hsl(135 35% 30%);
}
.chronicle-divider-changed .chronicle-divider-glyph {
  color: hsl(var(--ink) / 0.6);
}
.chronicle-divider-changed .chronicle-divider-glyph-center {
  color: hsl(var(--accent) / 0.55);
}
/* UI Notes #15 (M-9): per-ending closing tints. Default is ink (set in the
   base .chronicle-closing rule). Decimated red, Thriving green, Peaceful and
   Changed stay ink — matches the doc's mock_13 color story. */
.chronicle-page[data-ending="decimated"] .chronicle-closing {
  color: hsl(var(--accent));
}
.chronicle-page[data-ending="thriving"] .chronicle-closing {
  color: hsl(135 35% 28%);
}
.chronicle-page[data-ending="peaceful"] .chronicle-closing,
.chronicle-page[data-ending="changed"]  .chronicle-closing {
  color: hsl(var(--ink));
}

/* dc-Remix #11: standout villager's name in accent red + display font
   throughout the Chronicle prose. The portrait marginalia already uses
   the accent thread; this extends it into the prose so a reader visually
   tracks one villager from beginning to end. */
.chronicle-body .standout-name {
  color: hsl(var(--accent));
  font-family: var(--display);
  font-weight: 500;
  letter-spacing: 0.01em;
}
/* When the standout name sits INSIDE a .villager-hand span (from the
   M-1 hand-fonts pass), the hand font wins for typography but the
   standout color still applies — keep both threads readable. */
.chronicle-body .villager-hand .standout-name,
.chronicle-body .standout-name .villager-hand {
  color: hsl(var(--accent));
  font-family: inherit; /* let the hand font keep its identity */
}

.chronicle-footer {
  margin-top: 1.4rem;
  text-align: center;
}

.chronicle-closing {
  /* UI Notes #15 (M-9): doc mock_13 shows the closing line in italic
     sentence case (not uppercase) and tinted per ending kind:
       Decimated → red
       Thriving  → green
       Peaceful  → ink
       Changed   → ink (dashed-frame variant)
     Previously the rule applied red+uppercase to every ending, hiding the
     per-ending tone the doc requires. Default is ink; per-ending overrides
     below restore red/green where needed. */
  font-family: var(--serif);
  font-style: italic;
  font-size: 1.05rem;
  letter-spacing: 0.02em;
  color: hsl(var(--ink));
  text-transform: none;
}

.post-chronicle-controls {
  margin: 2.5rem 0;
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 0.9rem;
}

/* G-11 N7: gallery-overwrite note. Sits above the post-controls buttons,
   non-blocking, marginal voice. role="status" surfaces it to AT as a
   polite live region. Inside .post-chronicle-controls so it stays out of
   the exported PNG via data-no-export. The flex-basis: 100% line forces
   it onto its own row so the buttons stay centered below. */
.chronicle-overwrite-note {
  flex-basis: 100%;
  text-align: center;
  font-size: 0.9rem;
  color: hsl(var(--ink-faint));
  font-style: italic;
  margin: 0 0 0.4rem;
  max-width: 38rem;
  margin-left: auto;
  margin-right: auto;
  line-height: 1.4;
}

/* S2 (Phase 5) / PR #44 — Save Chronicle button.
   Visually subordinate to 'Start a new chronicle' (the natural exit from
   the chronicle screen). Post-PR-#44 the default .primary-button is the
   drawn-slot treatment (transparent fill, hairline rules above + below).
   The save button reads as a lighter aside by softening its rule weight
   so the two buttons sit side-by-side as a primary/secondary pair. */
.save-chronicle-button {
  color: hsl(var(--ink) / 0.78);
  border-top-color: hsl(var(--ink) / 0.35);
  border-bottom-color: hsl(var(--ink) / 0.35);
}
.save-chronicle-button:hover:not(:disabled) {
  color: hsl(var(--accent));
  border-top-color: hsl(var(--accent) / 0.6);
  border-bottom-color: hsl(var(--accent) / 0.6);
}
.save-chronicle-button:disabled {
  opacity: 0.7;
  cursor: progress;
}

/* Mobile: stack buttons rather than wrap awkwardly. */
@media (max-width: 480px) {
  .post-chronicle-controls {
    flex-direction: column;
    align-items: stretch;
    margin: 2rem 0.6rem;
  }
}

/* J-3 council: day-by-day log panel. Shown only on gallery replays
   (state.isReplay). Lives outside #chronicle-export-target so html-to-image
   Save Chronicle PNG is unchanged. Quiet archival register — IM Fell
   English for day headings, Lora serif for entries, IBM Plex Sans for the
   fallback note. max-height + overflow keeps mobile from stranding the
   controls offscreen. */
.day-by-day-log {
  max-width: 42rem;
  margin: 0.5rem auto 2.5rem;
  padding: 1.25rem 1.5rem 1.5rem;
  border-top: 1px solid hsl(var(--ink) / 0.22);
  max-height: 50vh;
  overflow-y: auto;
  font-family: var(--serif);
  color: hsl(var(--ink) / 0.92);
  line-height: 1.55;
}
.day-by-day-log[hidden] {
  display: none;
}
.day-log-day {
  margin-bottom: 1.25rem;
}
.day-log-day:last-child {
  margin-bottom: 0;
}
.day-log-day-heading {
  font-family: var(--display);
  font-size: 1.05rem;
  font-weight: 500;
  letter-spacing: 0.04em;
  color: hsl(var(--ink-soft));
  margin: 0 0 0.4rem;
}
.day-log-entries {
  list-style: none;
  padding: 0;
  margin: 0;
}
.day-log-entry {
  margin: 0 0 0.65rem;
  font-size: 0.95rem;
}
.day-log-entry:last-child {
  margin-bottom: 0;
}
.day-log-fallback-note {
  font-family: var(--sans);
  font-size: 0.78rem;
  font-style: italic;
  color: hsl(var(--ink-faint));
  text-align: center;
  margin: 0 0 1rem;
  padding-bottom: 0.75rem;
  border-bottom: 1px dashed hsl(var(--ink) / 0.18);
}
.day-log-toggle {
  /* Inherits from .text-button; small visual cue that it's archival. */
  font-family: var(--sans);
  font-size: 0.85rem;
}
@media (max-width: 480px) {
  .day-by-day-log {
    margin: 0.5rem 0.6rem 2rem;
    padding: 1rem 1rem 1.25rem;
    max-height: 60vh;
  }
}



/* ─────────── Animations (3 of 5 from Grok's set; 2 cut as integration-unsafe) */

/* Chronicle unroll animation lives on the .chronicle-page rule above (P8
   merged the duplicate block). Only the @keyframes definition stays here. */
@keyframes unroll {
  from {
    opacity: 0;
    transform: scaleY(0.1) rotateX(25deg);
    filter: blur(4px);
  }
  to {
    opacity: 1;
    transform: scaleY(1) rotateX(0);
    filter: blur(0);
  }
}

/* Buildings grow into existence in the village SVG */
.building {
  animation: growIn 0.8s cubic-bezier(0.34, 1.56, 0.64, 1) both;
  transform-origin: bottom center;
  transform-box: fill-box;
}

@keyframes growIn {
  from {
    opacity: 0;
    transform: scaleY(0.2) translateY(8px);
  }
  to {
    opacity: 1;
    transform: scaleY(1) translateY(0);
  }
}

/* Subtle accent pulse on consequential events the chronicler wants you to feel */
.log-entry[data-kind="MEETING_OUTCOME"],
.log-entry.entry-fresh[data-kind^="T18"],
.log-entry.entry-fresh[data-kind^="T21"] {
  animation: bloodPulse 2.4s ease 0.4s 1;
}

@keyframes bloodPulse {
  0%, 100% { border-left-color: hsl(var(--accent)); }
  50% { border-left-color: hsl(var(--accent) / 0.55); }
}

/* ─────────────────────────────────────────────────────────────────────
   Meeting outcome — rendered IN PLACE on the meeting screen after the
   player makes a choice, so the chronicler's reply is read on the same
   screen as the decision (not two clicks away in the village log).
   Mirrors .meeting-arrival.torn-prose so the outcome reads as the second
   half of the same scene. Added in PR #11 to close the post-choice
   transition gap identified in the usability audit.
   ───────────────────────────────────────────────────────────────────── */
.meeting-outcome {
  font-family: var(--serif);
  font-size: 1.1rem;
  line-height: 1.7;
  color: hsl(var(--ink-soft));
  padding: 1.9rem 2.1rem;
  margin-top: 1.5rem;
  margin-bottom: 1.75rem;
  background:
    radial-gradient(ellipse at 12% 18%, hsl(38 38% 92% / 0.5) 0%, transparent 45%),
    radial-gradient(ellipse at 88% 82%, hsl(36 32% 84% / 0.45) 0%, transparent 50%),
    hsl(38 36% 88%);
  /* Irregular polygon edges — same hand-torn fragment as the arrival,
     mirrored vertically so the two boxes feel like a matched pair. */
  clip-path: polygon(
    1% 1.5%, 4% 0.5%, 18% 1%, 31% 0.4%, 47% 0.9%, 62% 0.2%, 78% 0.7%, 91% 0.3%, 96% 0.6%, 100% 2%,
    99.4% 16%, 100% 30%, 99.6% 44%, 99.8% 62%, 99.4% 78%, 99.7% 94%, 98.5% 99.5%,
    93% 99.6%, 79% 99.9%, 62% 99.5%, 47% 99.8%, 32% 99.5%, 18% 99.7%, 9% 99.4%, 4% 99.8%, 1.5% 99%,
    0.5% 84%, 0% 68%, 0.4% 52%, 0% 36%, 0.5% 20%, 0% 8%
  );
  filter: drop-shadow(0 2px 6px hsl(0 0% 0% / 0.18));
}
.meeting-outcome p {
  margin-bottom: 0.85rem;
}
.meeting-outcome p:last-child {
  margin-bottom: 0;
}
.meeting-outcome p:first-child::first-letter {
  font-family: var(--display);
  font-size: 2.6rem;
  line-height: 1;
  float: left;
  padding: 0.15rem 0.5rem 0 0;
  color: hsl(var(--accent));
}

.meeting-return {
  display: flex;
  justify-content: center;
  margin-top: 1.5rem;
  margin-bottom: 0.5rem;
}

/* PR #46 — 600ms minimum read-time on the meeting outcome.
   The RETURN button renders visible but inert for the first 600ms so a
   reflexive click can't skip the run's emotional climax. State managed
   in JS: .meeting-return-btn-locked + aria-disabled="true" are added
   on render and removed after 600ms. Pointer-events: none stops mouse
   and touch; the click handler's aria-disabled check stops keyboard
   activation. Visual signal: lowered opacity + non-clickable cursor. */
.meeting-return-btn-locked {
  pointer-events: none;
  opacity: 0.55;
  cursor: default;
  /* Slight transition into the unlocked state so the button doesn't pop
     when it becomes active. */
  transition: opacity 280ms ease-out;
}

/* Choice grid fades out as the outcome takes its place. Pure opacity +
   small downward translate — no layout reflow. (.meeting-reveal is
   nested inside .meeting-choices since PR #15, so it fades with parent.) */
.meeting-choices.fading-out {
  opacity: 0;
  transform: translateY(6px);
  transition: opacity 240ms ease, transform 240ms ease;
  pointer-events: none;
}

.fading-in {
  animation: outcomeFadeIn 480ms ease-out both;
}
@keyframes outcomeFadeIn {
  from { opacity: 0; transform: translateY(8px); }
  to   { opacity: 1; transform: translateY(0); }
}

/* Respect reduced motion */
@media (prefers-reduced-motion: reduce) {
  .chronicle-page,
  .building,
  .log-entry {
    animation: none !important;
  }
  .meeting-choices.fading-out {
    transition: none;
  }
  .fading-in {
    animation: none;
  }
}

/* ─────────────────────────────────────────────────────────────────────
   Picker scroll cue (PR #12) — closes audit Friction A.
   On mobile, after the player selects the First Word, the SECOND WORD
   section label sits ~700px below the fold. Two reinforcing signals:

   1. .scroll-hint — small italic prompt directly under First Word grid,
      fades in only when A is selected and B is not yet. Disappears the
      moment B is clicked. Pure DOM toggle, no new state.

   2. .picker-summary becomes sticky-bottom below 720px so the live
      "Choose your words / A village of X and Y" summary + disabled
      Plant the Flag button is always visible — by itself implies the
      second word is required.
   ───────────────────────────────────────────────────────────────────── */

/* G-11 N8: scroll-hint bumped from 0.95rem → 1.1rem and ink-faint → ink
   so the Skeptic-persona "the hint is too small to notice" friction is
   resolved. Also widened max-width 32ch → 40ch so the hint reads as one
   continuous prompt instead of wrapping awkwardly at the arrow glyph. */
.scroll-hint {
  font-family: var(--serif);
  font-style: italic;
  font-size: 1.1rem;
  color: hsl(var(--ink));
  text-align: center;
  margin: 0.85rem auto 0;
  max-width: 40ch;
  /* Hidden by default, visible class fades it in. */
  opacity: 0;
  transform: translateY(-4px);
  transition: opacity 280ms ease, transform 280ms ease;
  pointer-events: none;
}
.scroll-hint.visible {
  opacity: 1;
  transform: translateY(0);
}

/* Sticky picker summary on mobile so the player always has a live
   readout of their selection and the disabled primary button as a
   continuous signal that more is required.

   Picker council follow-up (2026-05-13): with the duplicated-grid
   collapse, the sticky summary's overlap zone now sits over actual
   cards mid-list. Adding scroll-padding-bottom on the picker root
   ensures the last card clears the sticky element when scrolled to the
   end, and gives each in-list card a clean reading line above the
   sticky fade. */
@media (max-width: 720px) {
  #screen-picker .word-grid-section--single {
    padding-bottom: 5.5rem;
  }
  .picker-summary {
    position: sticky;
    bottom: 0;
    margin-top: 1.75rem;
    margin-bottom: 0;
    padding: 0.85rem 1rem 1.1rem;
    /* Soft parchment fade so the summary lifts off the page without a
       hard horizontal rule that would break the manuscript feel. */
    background: linear-gradient(
      to bottom,
      hsl(38 38% 86% / 0) 0%,
      hsl(38 38% 86% / 0.92) 22%,
      hsl(38 38% 86% / 1) 50%
    );
    backdrop-filter: blur(2px);
    z-index: 5;
  }
  #picker-summary-text {
    margin-bottom: 0.7rem;
    font-size: 1rem;
  }
  /* Footer needs space so it isn't permanently hidden under the sticky
     summary — push it above the summary in the natural flow. The sticky
     summary's `bottom: 0` plus the footer's `margin-bottom` reserves
     room when scrolled to the very end. */
  .picker-footer {
    margin-bottom: 1rem;
  }
}

@media (prefers-reduced-motion: reduce) {
  .scroll-hint {
    transition: none;
  }
}

/* PR #46 — mobile scroll cue on the Day 7 meeting screen.
   On phones the arrival prose (~700px) pushes the choice buttons below
   the fold; the cue appears after the prose and fades out once the
   player scrolls to the choices. Desktop has space for everything, so
   the cue is mobile-only. Adjacent rule below in the .meeting-scroll-hint
   block tightens spacing. */
.meeting-scroll-hint {
  display: none;
  margin: 1rem auto 0.6rem;
}
@media (max-width: 640px) {
  .meeting-scroll-hint {
    display: block;
  }
}

/* ─────────────────────────────────────────────────────────────────────
   PR #14 — In-run standout marker.
   From Day 10 onward, the villager who's currently leading the
   chronicle's standout scoring (kills*4, mourned*3, walks*2, gifts*2)
   gets a small red star superimposed on the top-right of their 40px
   portrait. Recomputed every refreshVillagers() so the marker can
   shift between days — watching it move IS the delight moment, the
   chronicle's protagonist emerging in real time.
   ───────────────────────────────────────────────────────────────────── */

.villagers-alive li.is-standout,
.villagers-dead li.is-standout {
  position: relative;
}

/* H5 (Phase 4) — root-cause fix.
   Audit flagged "the designed delight moment is invisible in play." Live
   testing confirmed two failure modes the audit missed:
   1. left:32px anchored to the LI's left edge, not the portrait's right
      edge — the star was rendering BEFORE the portrait, in the LI's left
      padding, where it visually merged with the page background.
   2. font-size 0.78rem (~12px) at 78% opacity made the star indistinguishable
      from a parchment grain spot at reading distance, even when correctly
      placed.

   Fix: anchor to the portrait via flexbox sibling rather than guessing
   pixel offsets. The portrait is the first child of the LI; we use
   `left` measured from the LI start = portrait width (40px) minus a
   small overlap so the star perches on the portrait's top-right corner.
   Bumped to 1.05rem (~17px) and full opacity — the star is now an
   intentional badge, not a pretend mark.

   The text-shadow is a soft warm-white halo so the deep-red star reads
   clean against any portrait line work behind it. */
.v-standout-mark {
  position: absolute;
  /* Portrait is 40px wide, sits at LI left padding. Star perches on its
     top-right with a 4px overhang so it reads as a marginalia badge. */
  top: -2px;
  left: 36px;
  z-index: 2;
  font-size: 1.05rem;
  line-height: 1;
  color: hsl(var(--accent));
  opacity: 1;
  /* Soft warm-white halo so the deep-red star lifts off the parchment
     without screaming. The halo also separates it from any portrait line
     work behind it. */
  text-shadow:
    0 0 4px hsl(38 50% 92% / 0.95),
    0 0 1px hsl(38 50% 92% / 0.95);
  pointer-events: auto;
  cursor: help;
  user-select: none;
  /* Gentle entry when the marker first appears or shifts villager. */
  animation: standoutMarkAppear 360ms ease-out both;
}

.villagers-dead li.is-standout .v-standout-mark {
  /* If the standout is dead (heretic kind), the marker stays but desaturates
     to match the grayscale portrait. Same chronicle weight, quieter colour. */
  color: hsl(var(--ink) / 0.6);
  opacity: 0.85;
}

/* K-1b council K-1, finding F4: meal-chosen villager mark.
   A subtle left-edge ink stroke on the <li>, visually subordinate to
   the ★ standout glyph. No color, no badge — just a quiet reminder that
   the chronicler has a face here. Both marks coexist on the same villager
   if they're both the meal-chosen AND the chronicle standout (the visual
   stack remains legible because the star sits on the portrait corner and
   the meal mark sits at the row's left edge). Reads from state.meal.chosenId
   only — never villager.mealStatus — per the F-4 fence and the K-1 decision
   doc §F4-B. */
.villagers-alive li.is-meal-chosen,
.villagers-dead li.is-meal-chosen {
  border-left: 2px solid hsl(var(--ink-faint));
  padding-left: 0.45rem;
}
.villagers-dead li.is-meal-chosen {
  /* If the meal-chosen villager later dies (heretic-purge, mourning,
     grudge), the mark stays but lightens to match the dead-row register.
     The consequence is exactly when the mark MATTERS (GPT condition 9). */
  border-left-color: hsl(var(--ink) / 0.25);
}

@keyframes standoutMarkAppear {
  from { opacity: 0; transform: translateY(-3px) scale(0.7); }
  to   { opacity: 1; transform: translateY(0) scale(1); }
}

@media (prefers-reduced-motion: reduce) {
  .v-standout-mark {
    animation: none;
  }
}

/* ─────────────────────────────────────────────────────────────────────
   PR #17 — Caveat prominence.
   The 'Your village may not obey' caveat was originally a footnote
   below the choice grid. The audit found it to be the single
   strongest copy moment in the game (players in Run 2 were
   genuinely surprised when their refusal was overridden) — but the
   surprise was only half-earned because the caveat read like a
   trailing footnote.
   Promoted to the second sentence of the prompt, set in larger
   italic deep-red, with a thin red rule above. The caveat now
   FRAMES the choice the player is about to make, not annotates it
   afterward.
   ───────────────────────────────────────────────────────────────────── */

.caveat-prominent {
  /* UI Notes #14 (May 2026): the caveat now renders as a left-bordered
     red block — the same LETHAL-entry / consequential-echo idiom (see
     .log-entry[data-bucket="LETHAL"] and .consequential-echo-quote).
     The doc mock_12 is explicit: the override-may-not-obey rule deserves
     a block container, not a centered banner. The player should read it
     as a rule, not a footnote. */
  font-family: var(--serif);
  font-style: italic;
  font-size: 1rem;
  line-height: 1.5;
  color: hsl(var(--accent));
  text-align: left;
  margin: 0.4rem auto 1.4rem;
  padding: 0.7rem 1rem 0.7rem 1.1rem;
  border: none;
  border-left: 4px solid hsl(var(--accent));
  background: hsl(var(--accent) / 0.08);
  max-width: 38rem;
}

/* ─────────────────────────────────────────────────────────────────────
   PR #16 — Day-passing animation.
   Triggered on every "Let the day pass" click. Brief (~480ms) page-edge
   shift suggesting a leaf of parchment turning, so time feels like
   it's moving rather than text just changing in place.

   Pure transform — no layout reflow, no extra render. The class is
   added on click and removed 520ms later. If the player double-clicks
   or the engine triggers a meeting/chronicle transition, the class
   either gets removed by its timeout or never fires (the new frame
   replaces the old).

   Subtle is the goal. The audit specifically warned against unrelated
   additions like sound or busy motion that would break the manuscript
   feel. We get the felt sense of a day passing by lifting the page
   edge briefly, not by overwhelming animation.
   ───────────────────────────────────────────────────────────────────── */

/* F#3 — Manuscript page-turn between days.
   Replaces the previous translateX-shift day-passing animation with a
   right-edge rotateY curl, calibrated against NN/g + web.dev guidance:
   450ms ease-out (NOT 600ms ease-in-out — see F3_dissent_research.md).
   backface-visibility:hidden + translateZ(1px) avoids the Safari
   stacking quirk; stopping at -165deg (not -180deg) keeps backface-
   visibility active mid-rotation and avoids the 90/180 flicker zone.
   Static linear-gradient overlay simulates the curl's shadow without
   the per-frame paint cost of animating box-shadow or gradient stops.
   .village-frame.no-curl is set by ui.js when the device fails the
   capability gate (low-mem touch device, hardwareConcurrency ≤4);
   that path falls back to a 100ms opacity cross-fade.
   The container needs `position:relative`, `perspective`, and `contain`
   so the transform stays GPU-composited and cannot propagate layout. */

.village-frame {
  position: relative;
  perspective: 1200px;
  transform-style: preserve-3d;
  contain: layout paint;
}

.village-frame.day-passing {
  transform-origin: right center;
  backface-visibility: hidden;
  animation: page-curl 450ms ease-out forwards;
}

/* Static shadow overlay — travels with the rotating page because it
   lives on a ::before pseudo-element of the rotating frame. No
   animation = no paint per frame. */
.village-frame.day-passing::before {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(to left, rgba(60, 40, 20, 0.18), transparent 12%);
  pointer-events: none;
  z-index: 2;
}

@keyframes page-curl {
  from { transform: rotateY(0deg) translateZ(0); }
  to   { transform: rotateY(-165deg) translateZ(1px); }
}

/* Cross-fade fallback for reduced-motion users and low-end devices.
   ui.js adds .no-curl on .village-frame when the capability gate
   fails (deviceMemory ≤ 4 on coarse-pointer devices, or
   hardwareConcurrency ≤ 4). 100ms is the NN/g "instantaneous" floor;
   any shorter and the eye won't register that a day passed. */
.village-frame.no-curl.day-passing {
  animation: page-fade 100ms ease-out forwards;
}
.village-frame.no-curl.day-passing::before { display: none; }

@keyframes page-fade {
  0%   { opacity: 1;    }
  50%  { opacity: 0.85; }
  100% { opacity: 1;    }
}

@media (prefers-reduced-motion: reduce) {
  /* Reduced motion always takes the cross-fade path regardless of the
     no-curl class state. */
  .village-frame.day-passing {
    animation: page-fade 100ms ease-out forwards;
    transform: none;
  }
  .village-frame.day-passing::before { display: none; }
}

/* ───────────────────────────────────────────────────────────────────────────
 * Watercolor scene images (Phase C)
 *
 * Static PNG scenes for the (wordA, wordB, phase) triple, loaded by
 * game/scenes.js via renderSceneImage(). Live alongside the SVG-village
 * fallback that is swapped in via <img onerror> if a scene file is missing.
 *
 * The image fills its slot at the same aspect ratio as the underlying SVG,
 * so all three slots (Day 1 village, Day 7 meeting, Day 14 chronicle) need
 * no layout changes.
 * ─────────────────────────────────────────────────────────────────────────── */
.scene-image {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: 16 / 9;
  object-fit: cover;
  border-radius: 1px;
  /* Subtle ink-edge feathering to blend with the parchment frame */
  filter: drop-shadow(0 1px 0 hsl(var(--parchment) / 0.6));
}

/* Day 7 meeting illustration — sits between the meeting header and the
   arrival prose. Scaled smaller than the Day 1 establish slot since the
   meeting screen is text-heavy. */
.meeting-illustration {
  margin: 0.6rem auto 1.2rem;
  max-width: 540px;
}
.meeting-illustration .scene-image {
  aspect-ratio: 16 / 9;
}

/* PR #46 — mobile meeting layout (UX audit #7).
   On mobile (≤640px) the meeting screen previously placed all three
   choice buttons below the fold: illustration + arrival prose + prompt
   + caveat ate ~890px before the first choice button began. Players
   couldn't see their options without scrolling, and the caveat "Your
   village may not obey" was the last thing in viewport before they
   had to scroll — amplifying the friction.

   Three small mobile-only changes pull the first choice into the
   initial viewport (≤844px target):
     1. illustration aspect-ratio loosens to 21:9 (less vertical space)
     2. illustration max-height capped so it can't dominate the screen
     3. arrival prose top-margin tightened
   The choice grid still scrolls naturally; this just ensures the first
   choice is visible when the meeting first appears. */
@media (max-width: 640px) {
  .meeting-illustration {
    margin: 0.3rem auto 0.6rem;
  }
  .meeting-illustration .scene-image {
    aspect-ratio: 21 / 9;
    max-height: 180px;
    object-fit: cover;
  }
  .meeting-arrival {
    margin-top: 0.4rem;
    margin-bottom: 0.6rem;
  }
  .meeting-header {
    margin-bottom: 0.4rem;
  }
}

/* Default scene-image rule for the small chronicle thumbnail. The hero
   variant (S2 Phase 5) overrides aspect-ratio earlier in the file to
   honor the watercolor's natural 5:4 ratio. */
.chronicle-illustration:not(.chronicle-illustration-hero) .scene-image {
  width: 100%;
  aspect-ratio: 16 / 9;
}

/* Reduced-motion / low-bandwidth: no special treatment needed since scenes
   are static PNGs, but respect contrast preferences. */
@media (prefers-contrast: more) {
  .scene-image {
    filter: contrast(1.08);
  }
}

/* ─────────────────────────────────────────────────────────────────────────
   PHASE 1 — accessibility & mobile UX (H6, H8, H9)
   ───────────────────────────────────────────────────────────────────────── */

/* H6 — Global :focus-visible ring.
   The audit found NO visible focus indicator on any interactive element when
   navigating by keyboard (Tab key). This violates WCAG 2.4.7 and breaks the
   game for screen-reader and keyboard-only users. One rule covers buttons,
   links, and any future interactive element; we use :focus-visible (not
   :focus) so mouse users don't see the ring on click but keyboard users do.
   Outline-offset 3px keeps the ring off the text glyphs and uses the
   existing accent (blood-red) so it reads as part of the manuscript palette
   rather than a generic browser default. */
:focus { outline: none; }
:focus-visible {
  outline: 2px solid hsl(var(--accent));
  outline-offset: 3px;
  border-radius: 2px;
}

/* ─────────────────────────────────────────────────────────────────────
   PR #47 — Chronicle gallery on the splash/picker.
   Ambient archive of completed chronicles. The grid is intentionally
   quiet — a parchment surface with small ink marks where chronicles
   exist — so a returning player feels the accumulation rather than
   reading a dashboard. F3 player research #3 rejected percentage
   counters and lock icons explicitly; we honor that here.

   Layout: aspect-ratio 1/1 cells in a grid. Desktop = 13 columns wide,
   7 rows tall = 91 cells. Mobile = 7 columns, 13 rows = same 91 cells
   rotated. Empty cells are visually subdued with a hairline border;
   filled cells get the ending-kind glyph in ink.
   ───────────────────────────────────────────────────────────────────── */
.chronicle-gallery {
  margin: 3rem auto 1rem;
  max-width: 720px;
  text-align: center;
}

/* G-8: diegetic completion counter. Sits between the section heading and
   the 91-cell grid as a small italic aside in the chronicler voice. Locked
   by the G-1 council decision: prose form ("N chronicles written · 91
   possible pages"), not sterile "N/91". --ink-faint is contrast-safe at
   the G-1 darkening (5.73:1 against parchment); explicit serif italic
   keeps it as marginalia rather than UI chrome. */
.chronicle-gallery-counter {
  margin: 0.5rem auto 0;
  font-family: var(--serif);
  font-style: italic;
  font-size: 0.9rem;
  color: hsl(var(--ink-faint));
  letter-spacing: 0.01em;
}

/* J-2 council J-2: completionist surface. Two registers:
   - .gallery-endings-seen: CHROME register (marginal, low-visual-weight)
     for the four ✓-marks above the counter. Distinct from the counter
     (which is already serif italic) so the two reads as metadata + voice.
   - .gallery-completionist-tail: VOICE register (italic serif, ink color)
     for the one chronicler-voice line about standout-soul kinds. Past-
     tense observation, never future-tense promise. Matches H-2 Mechanism
     E's prose register.
   See tools/council/J2_decision.md §"CSS register drift". */
.gallery-endings-seen {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 0.8rem 1.2rem;
  margin: 0.6rem auto 0;
  padding: 0;
  list-style: none;
  font-family: var(--sans);
  font-size: 0.78rem;
  letter-spacing: 0.02em;
}
.gallery-ending-item {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  color: hsl(var(--ink-faint));
  opacity: 0.55;
  transition: opacity 200ms ease;
}
.gallery-ending-item.seen {
  color: hsl(var(--ink));
  opacity: 1;
}
.gallery-ending-item .glyph {
  display: inline-flex;
  width: 14px;
  height: 14px;
}
.gallery-ending-item .glyph svg {
  width: 100%;
  height: 100%;
}
.gallery-ending-item .label {
  text-transform: lowercase;
}
.gallery-ending-item .check {
  /* The ✓ reinforces the seen state but is not the only signal (WCAG 1.4.1).
     Pairs with .seen class + textual aria-label. */
  font-size: 0.85em;
  margin-left: 0.1rem;
}
.gallery-completionist-tail {
  margin: 0.5rem auto 0;
  max-width: 38rem;
  text-align: center;
  font-family: var(--serif);
  font-style: italic;
  font-size: 0.92rem;
  color: hsl(var(--ink));
  opacity: 0.75;
  line-height: 1.5;
}

/* dc-Remix #13: bookshelf re-skin of the chronicle gallery. The 91 cells
   become book spines on a dark wooden shelf. Each spine is ~28px wide,
   height 65–85px varying by pair hash (computed in JS), color derived
   from the pair's tone combination (warm: amber/sepia, dark: deep
   red/charcoal, mixed: muted blue). The spine label is the two words in
   vertical writing-mode. Empty slots are ghost-outline spines.
   Re-skin only: click handlers and data-pair unchanged. */
.chronicle-gallery-grid {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: 3px 4px;
  margin: 1.4rem auto 0;
  padding: 1.1rem 1rem 0.6rem;
  background:
    /* Wood-grain gradient simulating a dark stained shelf */
    repeating-linear-gradient(90deg, hsl(28 22% 12%) 0, hsl(28 22% 12%) 1px, hsl(28 25% 16%) 2px, hsl(28 18% 13%) 4px),
    linear-gradient(180deg, hsl(28 24% 14%), hsl(28 20% 10%));
  border-radius: 3px;
  /* Shelf bottom edge */
  box-shadow:
    inset 0 -3px 0 hsl(28 30% 18%),
    inset 0 -6px 14px hsl(0 0% 0% / 0.55),
    0 4px 14px hsl(0 0% 0% / 0.35);
}
.chronicle-gallery-cell {
  /* Default cell is a ghost-outline spine: ~28px wide, height set inline */
  width: 28px;
  min-height: 65px;
  background: transparent;
  border: 1px dashed hsl(38 28% 60% / 0.22);
  border-radius: 1px 1px 0 0;
  padding: 0;
  margin: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  color: hsl(38 28% 70%);
  flex: 0 0 28px;
}
.chronicle-gallery-cell.filled.gallery-spine {
  background: var(--spine-bg, hsl(28 35% 28%));
  border: 1px solid hsl(0 0% 0% / 0.55);
  border-radius: 2px 2px 0 0;
  color: var(--spine-accent, hsl(36 50% 70%));
  cursor: pointer;
  position: relative;
  overflow: hidden;
  transition: transform 0.16s ease, box-shadow 0.16s ease, filter 0.16s ease;
  box-shadow:
    inset 0 0 6px hsl(0 0% 0% / 0.5),
    inset 1px 0 0 hsl(0 0% 100% / 0.05),
    0 1px 2px hsl(0 0% 0% / 0.55);
}
.chronicle-gallery-cell.filled.gallery-spine::before {
  /* Top gilt band suggesting a leather-bound spine */
  content: "";
  position: absolute;
  inset: 4px 3px auto 3px;
  height: 1px;
  background: var(--spine-accent, hsl(36 50% 70%));
  opacity: 0.55;
}
.chronicle-gallery-cell.filled.gallery-spine::after {
  /* Bottom gilt band, matching */
  content: "";
  position: absolute;
  inset: auto 3px 4px 3px;
  height: 1px;
  background: var(--spine-accent, hsl(36 50% 70%));
  opacity: 0.55;
}
.gallery-spine:hover {
  transform: translateY(-3px);
  filter: brightness(1.18);
  box-shadow:
    inset 0 0 6px hsl(0 0% 0% / 0.5),
    inset 1px 0 0 hsl(0 0% 100% / 0.08),
    0 5px 10px hsl(0 0% 0% / 0.6);
}
.gallery-spine:focus-visible {
  outline: 2px solid hsl(var(--accent));
  outline-offset: 1px;
}
.gallery-spine .spine-label {
  /* Vertical writing per the doc: two words stacked along the spine */
  writing-mode: vertical-rl;
  text-orientation: mixed;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 3px;
  padding: 7px 0;
  font-family: var(--display);
  font-size: 0.62rem;
  letter-spacing: 0.08em;
  white-space: nowrap;
  color: var(--spine-accent, hsl(36 50% 70%));
  text-shadow: 0 0 1px hsl(0 0% 0% / 0.85);
}
.gallery-spine .spine-word { line-height: 1; }
.gallery-spine .spine-amp {
  font-size: 0.55rem;
  opacity: 0.55;
  font-style: italic;
}
.gallery-spine .spine-glyph {
  position: absolute;
  bottom: 8px;
  left: 50%;
  transform: translateX(-50%);
  width: 10px;
  height: 10px;
  opacity: 0.65;
}
.gallery-spine .spine-glyph svg {
  width: 100%;
  height: 100%;
  display: block;
}
.chronicle-gallery-cell svg {
  width: 100%;
  height: 100%;
  display: block;
}

/* Mobile shelf — spines stay vertical, slightly narrower. */
@media (max-width: 640px) {
  .chronicle-gallery {
    margin: 2rem 0.5rem 0.5rem;
    max-width: none;
  }
  .chronicle-gallery-grid {
    padding: 0.8rem 0.6rem 0.45rem;
    gap: 2px 3px;
  }
  .chronicle-gallery-cell {
    width: 22px;
    flex: 0 0 22px;
    min-height: 56px;
  }
  .gallery-spine .spine-label {
    font-size: 0.5rem;
    letter-spacing: 0.05em;
    padding: 5px 0;
  }
}

/* ─────────────────────────────────────────────────────────────────────
   NH-C — Reading mode toggle (council R4, brief E #2).
   When the user opts into "modern" mode, we override the type stack to
   IBM Plex Sans, slash all animation durations down to 100ms cross-
   fades, and quiet the parchment-edge vignette so the page reads as a
   clean web document rather than a manuscript page. Palette tokens
   (—ink, —parchment, —accent) are kept unchanged so the visual identity
   survives the switch. Drop caps stay because the .dropcap span is a
   typographic affordance, not a font-only effect (council R16).

   The mode is applied on <html> via [data-reading-mode="modern"] from
   ui.js's applyReadingMode(). Defaults are the unannotated state.
   ───────────────────────────────────────────────────────────────────── */
html[data-reading-mode="modern"] {
  --serif: "IBM Plex Sans", system-ui, sans-serif;
  --display: "IBM Plex Sans", system-ui, sans-serif;
}

/* Slash motion durations site-wide. We don't disable motion entirely
   — a 100ms cross-fade still provides the felt-sense of a transition
   without forcing a slow read. */
html[data-reading-mode="modern"] * {
  animation-duration: 100ms !important;
  transition-duration: 100ms !important;
}

/* Quiet the parchment vignette + edge shadow in modern mode so the page
   reads as a clean document. Background color (parchment) is kept so
   the palette identity survives — just the texture stops. */
html[data-reading-mode="modern"] body {
  background: hsl(var(--parchment));
}
html[data-reading-mode="modern"] .page-frame,
html[data-reading-mode="modern"] .book-page {
  box-shadow: none !important;
}

/* In modern mode the chronicle title still feels chronicle-ish if we
   keep the display weight — but in sans it can crowd, so we relax
   letter-spacing and line-height slightly. */
html[data-reading-mode="modern"] .chronicle-title {
  letter-spacing: -0.005em;
  line-height: 1.15;
}

/* The drop cap survives via its own .dropcap span (PR #43). It still
   reads as a paragraph opener in sans; we just trim its over-elaborate
   serif rendering for the modern reader. */
html[data-reading-mode="modern"] .dropcap {
  font-family: var(--display);
  font-weight: 700;
}

/* Reading-mode toggle button — small italic text-button sitting in the
   picker footer. Reads as a chronicler aside ("and if you would prefer
   a different hand..."), not a settings switch. */
.reading-mode-toggle {
  display: block;
  margin: 0.8rem auto 0;
  font-family: var(--serif);
  font-style: italic;
  font-size: 0.9rem;
  /* G-1 fix: opacity 0.55 produced 3.54:1 contrast against parchment
     (fails WCAG 1.4.3 AA which requires 4.5:1 for normal text).
     Bumped to 0.78 to clear AA. */
  color: hsl(var(--ink) / 0.78);
  background: transparent;
  border: none;
  cursor: pointer;
  padding: 0.3rem 0.6rem;
  transition: color 0.18s ease;
}
.reading-mode-toggle:hover {
  color: hsl(var(--accent));
}
.reading-mode-toggle:focus-visible {
  outline: 1px dashed hsl(var(--accent));
  outline-offset: 4px;
}

/* Audio direction council 2026-05-13 — the chronicle music toggle.
   Inherits the same italic serif marginalia treatment as
   .reading-mode-toggle so the two controls live as a quiet pair
   beneath the chronicle. Same color, hover, and focus rules.
   Phrased restraint: this control should read as a parchment annotation,
   not a transport bar. */
.music-toggle {
  display: block;
  margin: 0.4rem auto 0;
  font-family: var(--serif);
  font-style: italic;
  font-size: 0.9rem;
  color: hsl(var(--ink) / 0.78);
  background: transparent;
  border: none;
  cursor: pointer;
  padding: 0.3rem 0.6rem;
  transition: color 0.18s ease;
}
.music-toggle:hover {
  color: hsl(var(--accent));
}
.music-toggle:focus-visible {
  outline: 1px dashed hsl(var(--accent));
  outline-offset: 4px;
}

/* PR #48 — share modal (manual-copy fallback for the share-grid).
   Used when navigator.clipboard.writeText is unavailable or denied.
   Renders a parchment card with a pre-selected textarea so the player
   can hit their platform's native copy gesture. Backdrop dismiss +
   Escape key + Done button all close it. Modal is the only place in
   the game where a real overlay appears — sized small, ink on parchment,
   no shadows that would break the manuscript voice. */
.share-modal {
  position: fixed;
  inset: 0;
  z-index: 100;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 1.5rem;
}
.share-modal-backdrop {
  position: absolute;
  inset: 0;
  background: hsl(var(--ink) / 0.45);
  cursor: pointer;
}
.share-modal-card {
  position: relative;
  background: hsl(var(--parchment));
  border: 1px solid hsl(var(--ink) / 0.55);
  padding: 1.6rem 1.8rem 1.4rem;
  max-width: 460px;
  width: 100%;
  display: flex;
  flex-direction: column;
  gap: 1rem;
  /* Subtle shadow only to lift the card off the dark backdrop; no
     drop-shadow flourish. */
  box-shadow: 0 8px 32px hsl(var(--ink) / 0.4);
}
.share-modal-title {
  font-family: var(--display);
  font-size: 1.4rem;
  font-weight: 400;
  margin: 0;
  color: hsl(var(--ink));
}
.share-modal-instructions {
  font-family: var(--serif);
  font-style: italic;
  font-size: 0.95rem;
  margin: 0;
  color: hsl(var(--ink) / 0.75);
}
.share-modal-text {
  font-family: ui-monospace, "SFMono-Regular", Menlo, Consolas, monospace;
  font-size: 1rem;
  line-height: 1.6;
  background: hsl(var(--parchment));
  border: 1px solid hsl(var(--ink) / 0.35);
  color: hsl(var(--ink));
  padding: 0.9rem 1rem;
  resize: none;
  width: 100%;
  min-height: 7em;
  /* The 14-glyph row reads correctly only in monospace; the surrounding
     header lines also benefit from the same column so the textarea
     doesn't shift visually as the user selects. */
}
.share-modal-text:focus {
  outline: 2px solid hsl(var(--accent));
  outline-offset: 2px;
}
.share-modal-close {
  align-self: flex-end;
  margin-top: 0.2rem;
}

/* M-11 PR-8 — modal hosts two actions (Copy text + Download card image)
   in a row, then a status line for clipboard / card feedback, then the
   text-button Done. Wrap to a stack on narrow screens. */
.share-modal-actions {
  display: flex;
  flex-direction: row;
  flex-wrap: wrap;
  gap: 0.6rem;
  align-items: stretch;
}
.share-modal-actions .primary-button {
  flex: 1 1 auto;
  min-width: 12em;
}
.share-modal-status {
  font-family: var(--serif);
  font-style: italic;
  font-size: 0.9rem;
  color: hsl(var(--ink) / 0.78);
  min-height: 1.4em;
  margin: 0;
}
@media (max-width: 480px) {
  .share-modal-actions { flex-direction: column; }
  .share-modal-actions .primary-button { width: 100%; min-width: 0; }
}

/* M-11 PR-8 — the OG capture shell lives at body level. It must
   contain the 1200×630 target with height:0 + overflow:hidden so the
   live page doesn't reserve layout space, while the inner target
   stays fully visible (opacity:1) for html-to-image's foreignObject
   clone to rasterize opaque pixels. The opacity:0 pattern that Plan v4
   originally specified produces a blank PNG — see
   tools/council/m11_pr8_og_card_dissent_research.md. */
.og-capture-shell {
  position: relative;
  height: 0;
  width: 0;
  overflow: hidden;
  pointer-events: none;
}

/* PR #47 — Day 1 absence marker.
   When priorRun() returns non-null, the village screen renders a single
   italic line at the bottom of the Day 1 log feed naming the prior run's
   standout (brief C cross-run absence marker). One-shot: only on Day 1
   of a new run, not after refresh. The line itself lives inside the log
   feed as a faint chronicler aside; the CSS just tones it down. */
.day1-absence-marker {
  font-family: var(--serif);
  font-style: italic;
  font-size: 0.95rem;
  color: hsl(var(--ink) / 0.6);
  text-align: center;
  margin: 1.4rem auto 0.4rem;
  padding: 0 1rem;
  max-width: 36ch;
}

/* H-2 Mechanism A: cross-run named-villager aside. Lives at top of log
   feed when a villager dies whose name matches a prior-run villager.
   Visually quieter than a log entry to read as marginal observation,
   not authored event. */
.cross-run-aside {
  font-family: var(--serif);
  font-style: italic;
  font-size: 0.9rem;
  color: hsl(var(--ink) / 0.55);
  text-align: center;
  margin: 1rem auto 0.4rem;
  padding: 0 1rem;
  max-width: 38ch;
  line-height: 1.5;
}

/* H-2 Mechanism E: seasonal closing line. Renders below the chronicle's
   existing closing paragraph. Slightly smaller / fainter so it reads as
   a Chronicler-coda, not a second closing line. */
.chronicle-seasonal-closing {
  font-family: var(--serif);
  font-style: italic;
  font-size: 0.92rem;
  color: hsl(var(--ink) / 0.6);
  text-align: center;
  margin: 1.1rem auto 0;
  padding: 0 1rem;
  max-width: 42ch;
  line-height: 1.5;
}

/* H8 — Mobile touch targets ≥ 44×44 CSS px (WCAG 2.5.5 / Apple HIG).
   The existing .primary-button on mobile shrinks to ~32px tall (0.65rem
   padding × 2 + 0.78rem text ≈ 33px). Tap accuracy on a phone falls off
   sharply below 44px. Same for word buttons in the picker, where players
   tap 14 of them at the start of every run. We override the existing
   mobile primary-button block with min-height + slightly looser padding
   that still preserves the nameplate look. */
@media (max-width: 640px) {
  .primary-button {
    min-height: 44px;
    padding: 0.85rem 1.2rem;
  }
  .word-button {
    min-height: 44px;
    padding: 0.95rem 0.9rem;
  }
  .text-button,
  .choice-button {
    min-height: 44px;
  }
}

/* H9 — Sticky "Let the day pass" on mobile.
   On phones the village log can run several screens; reaching the next-day
   action requires scrolling to the bottom each turn (logged 13× per run in
   the playthrough recording). Making the day-actions strip sticky to the
   viewport bottom keeps the action one tap away regardless of scroll
   position. We only apply it on mobile — on desktop the layout already
   shows the action without scrolling. The semitransparent parchment
   background prevents text from bleeding through; the top border line
   sells the strip as part of the page rather than a floating overlay. */
@media (max-width: 640px) {
  .village-day-actions {
    position: sticky;
    bottom: 0;
    z-index: 5;
    margin-top: 1.4rem;
    padding: 0.8rem 0.4rem max(0.8rem, env(safe-area-inset-bottom));
    background: hsl(var(--parchment) / 0.96);
    backdrop-filter: blur(2px);
    -webkit-backdrop-filter: blur(2px);
    border-top: 1px solid hsl(var(--ink) / 0.18);
  }
}

/* ─────────────────────────────────────────────────────────────────────────
 * F#1 — Stroke-reveal text engine
 *
 * The newest log entry's prose renders into a scaffold that pre-layouts
 * the full paragraph invisibly (so reveal can't cause mid-word wrap
 * jumps), with an overlay span that fills in character-by-character at
 * 30 cps. A sibling `.sr-only` node announces the full text to AT once
 * on completion.
 *
 * Council conditions encoded in CSS:
 *  - .reveal-pre keeps full layout reserved (visibility:hidden, NOT
 *    display:none — display:none would collapse the box).
 *  - .reveal-visible sits on top via grid stacking; nodeValue mutation
 *    is the only render-time write.
 *  - .sr-only is the standard AT-only utility; aria-live on the element
 *    handles announce-on-completion (StrokeReveal sets textContent once).
 *  - data-stroke-state="complete_not_advanceable" shows a subtle cursor
 *    pulse so the player understands the second click landed but the
 *    grace window is still open (GPT missed-risk #5).
 *  - user-select: none during reveal prevents selection-finish bugs
 *    (Opus missed-risk).
 *  - touch-action: pan-y lets vertical scroll pass through while we
 *    distinguish tap from scroll in JS via pointermove threshold.
 *  - Reduced motion already bypassed in JS; no CSS-only fallback needed.
 * ───────────────────────────────────────────────────────────────────────── */

.reveal-host {
  /* Stack the invisible pre-layout and the visible overlay in the same
     box. CSS Grid with a single cell is the simplest way to make two
     children occupy identical space without any positioning math. */
  display: grid;
  grid-template-columns: 1fr;
  touch-action: pan-y;
}
.reveal-host > .reveal-pre,
.reveal-host > .reveal-visible {
  grid-column: 1;
  grid-row: 1;
  white-space: pre-wrap; /* preserve newlines from htmlToPlain */
}
.reveal-host > .reveal-pre {
  /* Reserves layout (height, width, word-wrap geometry) without showing. */
  visibility: hidden;
}
.reveal-host > .reveal-visible {
  /* The character-revealed overlay. Renders on top. */
  position: relative;
}

/* Disable text selection while the reveal is animating so accidental
   mouseup-after-drag doesn't get interpreted as a click-to-finish. The
   moment data-revealed flips to true (in StrokeReveal._finalize), the
   article unlocks selection. */
.log-entry[data-revealing="true"] .reveal-host {
  user-select: none;
  -webkit-user-select: none;
}

/* Grace-window affordance: a subtle downward chevron pulses below the
   entry to signal "click again to continue". Disappears at advanceable
   (which arrives 500ms later, or sooner if grace was compressed). */
.log-entry[data-stroke-state="complete_not_advanceable"] .reveal-host::after {
  content: "▾";
  display: block;
  text-align: center;
  margin-top: 0.4rem;
  color: hsl(var(--ink) / 0.35);
  font-size: 0.85rem;
  animation: stroke-cursor-pulse 1s ease-in-out infinite;
}

@keyframes stroke-cursor-pulse {
  0%, 100% { opacity: 0.3; transform: translateY(0); }
  50%      { opacity: 0.7; transform: translateY(2px); }
}

@media (prefers-reduced-motion: reduce) {
  .log-entry[data-stroke-state="complete_not_advanceable"] .reveal-host::after {
    animation: none;
    opacity: 0.5;
  }
}

/* Cursor caret while actively revealing. Thin ink bar at the end of the
   currently-revealed substring. CSS-only — appended to the visible span
   via ::after, hidden in any other state. */
.log-entry[data-stroke-state="revealing"] .reveal-visible::after {
  content: "▎";
  display: inline-block;
  color: hsl(var(--ink) / 0.55);
  animation: stroke-caret-blink 0.9s steps(2) infinite;
  margin-left: 1px;
  font-weight: normal;
}
@keyframes stroke-caret-blink {
  0%, 50%  { opacity: 1; }
  51%, 100%{ opacity: 0; }
}
@media (prefers-reduced-motion: reduce) {
  .log-entry[data-stroke-state="revealing"] .reveal-visible::after {
    animation: none;
    opacity: 0.6;
  }
}

/* In modern reading mode the stroke-reveal is bypassed entirely (JS sets
   data-reading-mode="modern" on <html>, shouldBypassReveal() returns
   true, and the entry renders with the legacy .log-text path). The
   scaffold classes never fire in that mode, so no overrides needed. */

/* Standard sr-only utility (used by the aria-live announcement node).
   Already may exist elsewhere; harmless if duplicated. */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0,0,0,0);
  white-space: nowrap;
  border: 0;
}

/* ─────────────────────────────────────────────────────────────────────────
 * F#2 — Marginalia footnote system
 *
 * After F#1's stroke-reveal completes naturally on a log entry, if the
 * entry carries a marginalia object, this aside fades in (300ms ease-out)
 * in the entry's bottom margin. On Day 14, the chronicle reads back
 * every marginalia as a "What stayed" paragraph in P6.
 *
 * Council rules encoded in CSS:
 *  - Mobile: inline bottom-of-entry, full-width, right-aligned italic
 *  - Desktop: same position (bottom of entry, right-aligned italic) —
 *    the spec said "margin-right on desktop" but our log-entry doesn't
 *    have a side gutter; bottom-of-entry right-aligned achieves the
 *    same manuscript-marginalia visual without competing with the
 *    sticky day-pass button on mobile.
 *  - 70% opacity baseline (the chronicler's lighter ink)
 *  - data-marginalia-pending="true" → opacity 0
 *  - data-marginalia-rendered="true" → opacity 0.7 (faded ink)
 *  - 300ms ease-out transition
 *  - Modern reading mode and reduced motion: skip the fade
 * ───────────────────────────────────────────────────────────────────────── */

.marginalia {
  display: flex;
  align-items: baseline;
  justify-content: flex-end;
  gap: 0.45rem;
  margin: 0.6rem 0 0 0;
  padding-top: 0.5rem;
  border-top: 1px dashed hsl(var(--ink) / 0.18);
  font-family: "Lora", Georgia, serif;
  font-style: italic;
  font-size: 0.88rem;
  line-height: 1.45;
  color: hsl(var(--ink) / 0.72);
  text-align: right;
  opacity: 0.7;
  transition: opacity 0.3s ease-out;
}

/* Hidden during stroke-reveal, before complete_not_advanceable */
.marginalia[data-marginalia-pending="true"] {
  opacity: 0;
  pointer-events: none;
}

/* Rendered = visible. Used both by the fade-in handler and by the
   initial-render path for already-revealed entries (replay/gallery). */
.marginalia[data-marginalia-rendered="true"] {
  opacity: 0.7;
}

.marginalia-glyph {
  font-size: 0.95rem;
  color: hsl(var(--accent) / 0.7);
  flex: 0 0 auto;
}

.marginalia-note {
  flex: 1 1 auto;
  max-width: 38ch;
}

/* Modern reading mode: no fade, ever. Render at full opacity always
   (data-marginalia-rendered may not be set on a freshly mounted
   newest-entry, but in modern mode shouldBypassReveal() is true so
   logEntryHTML sets data-marginalia-rendered up front). The override
   below is a safety net for the edge case where the data attribute
   path doesn't fire. */
html[data-reading-mode="modern"] .marginalia {
  transition: none;
  opacity: 0.85;
}
html[data-reading-mode="modern"] .marginalia-glyph {
  color: hsl(var(--ink) / 0.55);
}

/* Reduced motion: no fade transition; the data-marginalia-pending state
   collapses instantly. */
@media (prefers-reduced-motion: reduce) {
  .marginalia {
    transition: none;
  }
}

@media (max-width: 640px) {
  .marginalia {
    /* On mobile, full-width inline (still right-aligned). Smaller
       font so a 6-note run on a small screen doesn't push the
       sticky day-pass below the fold. */
    font-size: 0.82rem;
    margin-top: 0.5rem;
    padding-top: 0.4rem;
  }
  .marginalia-note {
    max-width: none;
  }
}

/* ─────────────────────────────────────────────────────────────────────────
 * F#4 — Meal pick screen
 *
 * The Day 7 long-table beat. Renders candidate villager portraits in a
 * grid; player taps to select (visual highlight), then taps "Share the
 * meal" to confirm. Mobile: same flow. Desktop: drag-to-table is a
 * deferred enhancement; tap-then-tap is the council-canonical path.
 * ───────────────────────────────────────────────────────────────────────── */

.meal-frame {
  /* Same parchment-page treatment as meeting-frame */
}

.meal-header {
  text-align: center;
  margin-bottom: 1rem;
}

.meal-illustration {
  margin: 1rem auto;
  max-width: 720px;
}

/* UI Notes #12 (May 2026): the long table.
   The candidates ROW sits above a 5px-tall dark matte band on a dark
   background. The chronicler's empty chair is a small ghosted slot at
   the row's near side. An italic accent-amber sublabel reads beneath.

   Doc mock_10 is the reference; this implementation reuses the existing
   .meal-candidate cards unchanged so the contract / a11y / selection
   flow stays intact. The dark matte uses the same parchment-vs-ink
   token system as the rest of the manuscript (no new tokens). */
.long-table {
  position: relative;
  margin: 1.4rem auto;
  padding: 1.4rem 1.2rem 0.6rem;
  background:
    /* faint diagonal grain for matte texture */
    repeating-linear-gradient(135deg, hsl(28 18% 12% / 0.0) 0 6px, hsl(28 22% 8% / 0.18) 6px 7px),
    linear-gradient(180deg, hsl(28 18% 11%), hsl(28 16% 8%));
  border-radius: 4px;
  box-shadow:
    inset 0 1px 0 hsl(28 30% 22% / 0.4),
    inset 0 -2px 0 hsl(28 24% 5%),
    0 4px 14px hsl(0 0% 0% / 0.35);
}
/* The 5px-tall table line itself — a single horizontal band beneath the
   portraits, slightly lighter than the matte so it reads as carved wood. */
.long-table::after {
  content: "";
  position: absolute;
  left: 8%;
  right: 8%;
  bottom: 2.6rem;
  height: 5px;
  background: linear-gradient(180deg, hsl(28 35% 22%), hsl(28 28% 14%));
  border-radius: 2px;
  box-shadow:
    0 1px 0 hsl(28 35% 28% / 0.6),
    0 4px 8px hsl(0 0% 0% / 0.5);
  pointer-events: none;
}
.long-table .meal-candidates {
  margin: 0 0 1rem;
  position: relative;
  z-index: 1;
}
/* Sublabel: italic accent-amber-on-matte, reads as parchment etched into
   the table edge. */
.long-table-sublabel {
  position: relative;
  z-index: 1;
  margin: 0;
  text-align: center;
  font-family: var(--serif);
  font-style: italic;
  font-size: 0.85rem;
  color: hsl(36 45% 60% / 0.85);
  letter-spacing: 0.02em;
}
/* The chronicler's empty chair: a ghosted slot occupying one grid cell.
   When the meal-candidates is grid auto-fit, this becomes the 'far end'
   slot opposite the candidates, conveying the village-vs-chronicler
   spatial reading. */
.chronicler-seat {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 140px;
  border: 1.5px dashed hsl(36 30% 50% / 0.35);
  border-radius: 6px;
  color: hsl(36 30% 55% / 0.6);
  font-family: var(--serif);
  font-style: italic;
  font-size: 0.75rem;
}
.chronicler-seat::before {
  content: "the chronicler";
}
/* On the dark matte, the candidate cards need to invert: parchment fill,
   ink text. The existing rules (transparent bg, ink-color border) leave
   them dark-on-dark and unreadable. */
.long-table .meal-candidate {
  background: hsl(var(--parchment));
  border-color: hsl(28 35% 22%);
  color: hsl(var(--ink));
}
.long-table .meal-candidate:hover {
  background: hsl(var(--parchment));
  border-color: hsl(var(--accent) / 0.6);
  transform: translateY(-2px);
}
.long-table .meal-candidate.selected {
  border-color: hsl(var(--accent));
  background: hsl(var(--parchment));
  box-shadow: 0 0 0 2px hsl(var(--accent) / 0.18);
}

.meal-candidates {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
  gap: 0.9rem;
  margin: 1.4rem 0;
}

.meal-candidate {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.4rem;
  padding: 0.7rem 0.5rem 0.9rem;
  background: transparent;
  border: 1.5px solid hsl(var(--ink) / 0.18);
  border-radius: 6px;
  cursor: pointer;
  transition: background 200ms ease-out, border-color 200ms ease-out, transform 200ms ease-out;
  font-family: "Lora", Georgia, serif;
  color: hsl(var(--ink));
}

.meal-candidate:hover {
  background: hsl(var(--ink) / 0.04);
  border-color: hsl(var(--ink) / 0.5);
}

.meal-candidate:focus-visible {
  background: hsl(var(--ink) / 0.04);
  border-color: hsl(var(--ink) / 0.5);
  /* G-1 fix: restore the focus ring suppressed by the previous
     `outline: none` declaration. WCAG 2.4.7 Focus Visible (AA). */
  outline: 2px solid hsl(var(--accent));
  outline-offset: 2px;
}

.meal-candidate.selected {
  background: hsl(var(--accent) / 0.08);
  border-color: hsl(var(--accent) / 0.7);
  transform: translateY(-2px);
}

.meal-candidate[disabled] {
  opacity: 0.5;
  cursor: not-allowed;
}

.meal-candidate-portrait {
  display: block;
  width: 80px;
  height: 80px;
}

.meal-candidate-name {
  font-weight: 600;
  font-size: 0.95rem;
}

.meal-candidate-sigil {
  font-size: 0.78rem;
  font-style: italic;
  color: hsl(var(--ink) / 0.6);
}

.meal-confirm {
  text-align: center;
  margin-top: 1.2rem;
}

.meal-outcome {
  margin: 1rem 0;
  font-family: "Lora", Georgia, serif;
  line-height: 1.6;
}

.meal-outcome .meal-monologue {
  display: block;
  margin: 1rem 1.2rem;
  padding: 0.6rem 0.8rem 0.6rem 1.2rem;
  border-left: 2px solid hsl(var(--accent) / 0.5);
  font-style: italic;
}

@media (max-width: 640px) {
  .meal-candidates {
    grid-template-columns: repeat(2, 1fr);
  }
  .meal-candidate-portrait {
    width: 64px;
    height: 64px;
  }
}


/* ─────────── F#5 — Sigil-placement ritual (Anvil at Dawn) ─────────── */

/* The ritual screen sits between the picker and Day 1 as a UI gate. Goal:
   make placing the two soul-word sigils on the anvil read as a *physical
   act of consecration* (Cultist Simulator card-slot grammar) rather than
   a menu confirmation. See game/sigil-ritual.js and tools/council/F5_decision.md. */

#screen-sigil-ritual .ritual-frame {
  max-width: 720px;
  margin: 0 auto;
  padding: 2.4rem 1.4rem 3rem;
  text-align: center;
}

.ritual-header {
  margin-bottom: 1.8rem;
}

.ritual-title {
  font-family: "IM Fell English", Georgia, serif;
  font-size: clamp(1.6rem, 4.5vw, 2.2rem);
  color: hsl(var(--ink));
  margin: 0 0 0.5rem;
  letter-spacing: 0.01em;
}

.ritual-subtitle {
  color: hsl(var(--ink-soft));
  font-size: 0.95rem;
  margin: 0;
  font-style: italic;
}

/* Stage: anvil scene above, sigil dock below (NN/g thumb-zone majority
   over Opus's side-flanked composition; see F5_dissent_research.md). */
.ritual-stage {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 1.4rem;
  margin: 1.6rem 0;
}

.anvil-panel {
  position: relative;
  width: min(360px, 80vw);
  aspect-ratio: 320 / 200;
  color: hsl(var(--ink));
  transition: filter 200ms ease-out;
}

.anvil-panel .anvil-scene {
  width: 100%;
  height: 100%;
  display: block;
}

/* Hit-target extends 20% beyond the visual anvil on all sides
   (Fitts's Law forgiveness). Invisible but accepts pointer events
   when sigil is lifted.

   K-1a council K-1, finding F8 (Skeptic touch-target bug): the
   inset:-20% overlay was intercepting taps on the second sigil card
   below it on narrow viewports because the hit zone extended into
   the sigil-dock area. The hit target SHOULD only accept pointer
   events when a sigil has been lifted (i.e. the anvil is .anvil-receptive
   per game/sigil-ritual.js line 253). Outside that state, the overlay
   stays geometrically large for Fitts's-law forgiveness on the
   subsequent tap but releases pointer events so it doesn't steal
   taps from the cards below. */
.anvil-hit-target {
  position: absolute;
  inset: -20%;
  border-radius: 12px;
  cursor: pointer;
  /* Floor at 44x44 CSS px per WCAG 2.5.8 enhanced target size */
  min-width: 44px;
  min-height: 44px;
  /* K-1a F8: only intercept pointer events when the anvil is
     actively waiting for a sigil. */
  pointer-events: none;
}

.anvil-panel.anvil-receptive .anvil-hit-target {
  pointer-events: auto;
}

.anvil-panel.anvil-receptive .anvil-scene {
  filter: drop-shadow(0 0 6px hsl(var(--accent, var(--ink)) / 0.4));
}

.anvil-panel[data-state="one"] .anvil-scene,
.anvil-panel[data-state="complete"] .anvil-scene {
  opacity: 0.95;
}

/* Anvil slots: where the placed sigils sit on the anvil's flat top.
   The anvil scene's top face runs roughly from x=110+24=134 to x=110+86=196
   at y≈76 in the 320×200 viewBox; positioning the slots over that band
   so the placed sigils land ON the anvil rather than floating above it. */
.anvil-slots {
  position: absolute;
  /* The anvil's flat top sits roughly at 60% of the panel's visible height
     (the SVG viewBox is 320×200, anvil top face at y=24+100=124 → 124/200=62%).
     Center the slots over that band. */
  top: 58%;
  left: 42%;
  right: 42%;
  display: flex;
  justify-content: space-between;
  gap: 0.2rem;
  pointer-events: none;
}

.anvil-slot {
  width: 24px;
  height: 24px;
  position: relative;
}

.anvil-placed-glyph {
  display: block;
  width: 100%;
  height: 100%;
  color: hsl(var(--ink));
}

.anvil-placed-glyph .sigil-glyph-svg {
  width: 100%;
  height: 100%;
}

/* Sigil dock — two cards below the anvil. */
.sigil-dock {
  display: flex;
  gap: 1.2rem;
  justify-content: center;
  align-items: stretch;
  flex-wrap: wrap;
  max-width: 100%;
}

.sigil-card {
  position: relative;
  background: hsl(var(--parchment-deep));
  border: 1px solid hsl(var(--ink) / 0.3);
  border-radius: 6px;
  padding: 0.9rem 1rem;
  min-width: 140px;
  min-height: 130px;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 0.4rem;
  cursor: grab;
  color: hsl(var(--ink));
  font-family: inherit;
  transition: transform 200ms ease-out, box-shadow 200ms ease-out, opacity 150ms ease-out;

  /* iOS safety per F5_decision.md condition 4 */
  touch-action: none;
  -webkit-user-select: none;
  user-select: none;
  -webkit-touch-callout: none;

  box-shadow: 0 1px 0 hsl(var(--parchment-edge) / 0.5);
}

.sigil-card:hover:not([data-state="placed"]) {
  background: hsl(var(--parchment));
  border-color: hsl(var(--ink) / 0.5);
}

.sigil-card:focus-visible {
  outline: 2px solid hsl(var(--ink) / 0.8);
  outline-offset: 3px;
}

.sigil-card[data-state="lifted"] {
  transform: translateY(-4px) scale(1.04);
  box-shadow: 0 4px 12px hsl(var(--ink) / 0.18);
  background: hsl(var(--parchment));
  z-index: 5;
}

.sigil-card[data-state="dragging"] {
  cursor: grabbing;
  box-shadow: 0 8px 20px hsl(var(--ink) / 0.28);
  background: hsl(var(--parchment));
  z-index: 10;
}

.sigil-card[data-state="placed"] {
  opacity: 0.35;
  cursor: default;
  pointer-events: none;
}

.sigil-card .sigil-glyph {
  display: block;
  width: 36px;
  height: 36px;
  color: hsl(var(--ink));
}

.sigil-card .sigil-glyph-svg {
  width: 100%;
  height: 100%;
}

.sigil-card .sigil-name {
  font-family: "IM Fell English", Georgia, serif;
  font-size: 1rem;
  letter-spacing: 0.02em;
}

.sigil-card .sigil-tagline {
  font-size: 0.78rem;
  color: hsl(var(--ink-faint));
  font-style: italic;
  max-width: 18ch;
  line-height: 1.25;
}

/* Radial countdown ring during long-press (350ms expressive primary). */
.sigil-radial {
  position: absolute;
  inset: 0;
  pointer-events: none;
  opacity: 0;
  display: flex;
  align-items: center;
  justify-content: center;
}

.sigil-radial-svg {
  width: 80%;
  height: 80%;
}

.sigil-radial-svg circle {
  fill: none;
  stroke: hsl(var(--ink) / 0.55);
  stroke-width: 1.5;
  stroke-dasharray: 100.5; /* 2π*16 ≈ 100.5 */
  stroke-dashoffset: 100.5;
  transform: rotate(-90deg);
  transform-origin: 50% 50%;
}

.sigil-card.sigil-charging .sigil-radial {
  opacity: 1;
}

.sigil-card.sigil-charging .sigil-radial-svg circle {
  animation: sigilRadialFill 350ms linear forwards;
}

@keyframes sigilRadialFill {
  to { stroke-dashoffset: 0; }
}

/* Snap-back animation when sigil is dropped off-target (200ms ease-back) */
.sigil-card.sigil-snap-back {
  animation: sigilSnapBack 200ms cubic-bezier(0.34, 1.56, 0.64, 1) both;
}

@keyframes sigilSnapBack {
  0%   { transform: translate(var(--snap-from-x, 0), var(--snap-from-y, 0)) scale(1.02); }
  70%  { transform: translate(0, 0) scale(0.98); }
  100% { transform: translate(0, 0) scale(1); }
}

/* One-shot pulse hint to teach the gesture on the second sigil
   after the first lands, and to teach tap-then-tap when the player
   taps the anvil without lifting first. */
.sigil-card.sigil-pulse-hint {
  animation: sigilPulse 600ms ease-in-out both;
}

@keyframes sigilPulse {
  0%, 100% { transform: scale(1); }
  50%      { transform: scale(1.04); box-shadow: 0 4px 14px hsl(var(--ink) / 0.16); }
}

/* "The seal is set" caption appears after both sigils placed */
.ritual-caption {
  font-family: "IM Fell English", Georgia, serif;
  font-size: 1.05rem;
  color: hsl(var(--ink));
  letter-spacing: 0.04em;
  font-variant: small-caps;
  margin: 1rem 0 0.6rem;
  opacity: 0;
  transition: opacity 400ms ease-out;
  min-height: 1.3em;
}

.ritual-caption.visible {
  opacity: 1;
}

/* Confirm button (drawn-slot styling — game's standard button vocabulary).
   Hidden until both sigils placed and caption settles. Once visible, a
   600ms wet-ink underline draws under the label per F5_decision Q6. */
.ritual-confirm {
  position: relative;
  display: inline-flex;
  flex-direction: column;
  align-items: center;
  gap: 0.4rem;
  background: transparent;
  border: none;
  border-top: 1px solid hsl(var(--ink) / 0.55);
  border-bottom: 1px solid hsl(var(--ink) / 0.55);
  color: hsl(var(--ink));
  font-family: "IM Fell English", Georgia, serif;
  font-size: 1.1rem;
  letter-spacing: 0.04em;
  padding: 0.7rem 2.2rem;
  cursor: pointer;
  opacity: 0;
  pointer-events: none;
  transition: opacity 400ms ease-out, color 200ms ease-out, border-color 200ms ease-out;
}

.ritual-confirm.revealed {
  opacity: 1;
  pointer-events: auto;
}

.ritual-confirm:hover:not(:disabled) {
  color: hsl(var(--accent, var(--ink)));
  border-top-color: hsl(var(--accent, var(--ink)) / 0.7);
  border-bottom-color: hsl(var(--accent, var(--ink)) / 0.7);
}

.ritual-confirm:focus-visible {
  outline: 2px solid hsl(var(--ink) / 0.8);
  outline-offset: 4px;
}

.ritual-confirm:disabled {
  cursor: default;
}

/* Wet-ink underline that draws under the label after both blooms complete */
.ritual-confirm-underline {
  display: block;
  width: 0;
  height: 1px;
  background: hsl(var(--accent, var(--ink)));
  transition: width 600ms ease-out;
  margin-top: 2px;
}

.ritual-confirm.inking .ritual-confirm-underline {
  width: 70%;
}

.ritual-footer {
  margin-top: 1.4rem;
  display: flex;
  justify-content: center;
}

/* Narrow viewports: keep the dock horizontal (per dissent research, side
   stacking on phones < 400px keeps the button above the fold). */
@media (max-width: 400px) {
  .sigil-dock {
    gap: 0.6rem;
  }
  .sigil-card {
    min-width: 130px;
    padding: 0.7rem 0.6rem;
  }
}

/* Reduced-motion + modern reading mode bypass: instant transitions,
   no radial countdown, no bloom, no snap-back curve. The ritual still
   happens — the ceremony compresses. */
.ritual-bypass .sigil-radial,
.ritual-bypass .sigil-card.sigil-snap-back,
.ritual-bypass .sigil-card.sigil-pulse-hint {
  animation: none !important;
}

.ritual-bypass .sigil-card {
  transition-duration: 100ms;
}

.ritual-bypass .ritual-caption {
  transition-duration: 100ms;
}

.ritual-bypass .ritual-confirm-underline {
  transition-duration: 100ms;
}

@media (prefers-reduced-motion: reduce) {
  .sigil-card,
  .sigil-radial,
  .ritual-caption,
  .ritual-confirm,
  .ritual-confirm-underline {
    transition-duration: 100ms !important;
    animation-duration: 100ms !important;
  }
  .sigil-card.sigil-charging .sigil-radial {
    opacity: 0;
  }
}


/* ─────────── F#7 — Chronicle scroll-paced reveal ─────────── */

/* The reveal toolbar sits in the upper-right corner of the chronicle
   screen — visible from frame one. Houses the play/pause toggle and the
   "Show as page" escape valve (WCAG 2.2.2 mandate for auto-moving
   content >5s). The toolbar is excluded from PNG export via
   [data-no-export]. */
.reveal-toolbar {
  position: fixed;
  top: 1rem;
  right: 1rem;
  display: flex;
  gap: 0.6rem;
  align-items: center;
  z-index: 50;
  background: hsl(var(--parchment-deep) / 0.92);
  border: 1px solid hsl(var(--ink) / 0.25);
  border-radius: 6px;
  padding: 0.4rem 0.6rem;
  box-shadow: 0 2px 8px hsl(var(--ink) / 0.12);
  backdrop-filter: blur(2px);
}

/* G-9: visible deferral promise for backward step. Opus condition 4 of the
   G-2 council — keeps the Drifter persona's request acknowledged without
   shipping the rewind feature itself. Sits inside the reveal toolbar so it
   surfaces only during reveal, not on the static post-controls. */
.reveal-rewind-promise {
  margin: 0;
  padding-left: 0.6rem;
  border-left: 1px solid hsl(var(--ink) / 0.18);
  font-family: var(--serif);
  font-style: italic;
  font-size: 0.72rem;
  color: hsl(var(--ink-faint));
  white-space: nowrap;
  align-self: center;
  opacity: 0.85;
}

/* On small viewports the toolbar wraps; hide the promise to avoid crowding. */
@media (max-width: 600px) {
  .reveal-rewind-promise { display: none; }
}

/* G-11 N2: bump 32px → 44px for WCAG 2.5.8 AAA target size. AA threshold
   is 24px; AAA is 44px. Pause/play is a low-frequency control on a busy
   reveal surface, so the bigger target trades cleanly against any visual
   weight cost. The glyph itself stays the same size; only the hit area
   grew. */
.reveal-playpause {
  background: transparent;
  border: none;
  cursor: pointer;
  font-size: 1.1rem;
  color: hsl(var(--ink));
  width: 44px;
  height: 44px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: 4px;
  transition: opacity 800ms ease-out;
}

.reveal-playpause.reveal-faded {
  opacity: 0.4;
}

.reveal-playpause:hover,
.reveal-playpause:focus-visible {
  opacity: 1 !important;
  background: hsl(var(--ink) / 0.06);
}

.reveal-playpause:focus-visible,
.reveal-show-as-page:focus-visible,
.replay-cinematic-link:focus-visible {
  outline: 2px solid hsl(var(--ink) / 0.8);
  outline-offset: 2px;
}

.reveal-glyph {
  font-size: 1.05rem;
}

.reveal-show-as-page {
  background: transparent;
  /* G-1 fix: border at 0.4 alpha produced 2.38:1 against parchment.
     Raised to 0.8 alpha to clear WCAG 1.4.11 Non-text Contrast (3:1 AA). */
  border: 1px solid hsl(var(--ink) / 0.8);
  border-radius: 4px;
  font-family: inherit;
  font-size: 0.85rem;
  color: hsl(var(--ink));
  padding: 0.35rem 0.7rem;
  cursor: pointer;
  transition: background 150ms ease-out, color 150ms ease-out;
}

.reveal-show-as-page:hover {
  background: hsl(var(--ink));
  color: hsl(var(--parchment));
}

/* Chronicle is in revealing state: starts at opacity 0 then fades in.
   The actual scroll is driven by JS in chronicle-reveal.js. */
.chronicle-page.chronicle-revealing {
  /* Inline opacity controls the fade; no rule needed here beyond keeping
     transitions snappy. The cinematic JS owns the timing. */
}

/* Reveal-cinematically link sits below the static chronicle, only shown
   for previously-viewed runs (or after the player escapes the cinematic).
   Drawn-slot styling consistent with the rest of the game's controls. */
.replay-cinematic-link {
  display: inline-block;
  margin: 1.4rem auto 0.4rem;
  background: transparent;
  border: none;
  border-top: 1px solid hsl(var(--ink) / 0.3);
  border-bottom: 1px solid hsl(var(--ink) / 0.3);
  color: hsl(var(--ink) / 0.7);
  font-family: "IM Fell English", Georgia, serif;
  font-size: 0.9rem;
  padding: 0.45rem 1.4rem;
  cursor: pointer;
  letter-spacing: 0.02em;
  font-style: italic;
}

.replay-cinematic-link:hover {
  color: hsl(var(--accent, var(--ink)));
  border-top-color: hsl(var(--accent, var(--ink)) / 0.55);
  border-bottom-color: hsl(var(--accent, var(--ink)) / 0.55);
}

/* Post-controls fade-in is controlled by the reveal JS via inline opacity.
   Add a transition so the JS-applied opacity:1 fades smoothly. */
.post-chronicle-controls {
  transition: opacity 600ms ease-out;
}

/* Mobile: toolbar shrinks to fit narrow viewports without colliding
   with the article title. */
@media (max-width: 480px) {
  .reveal-toolbar {
    top: 0.6rem;
    right: 0.6rem;
    padding: 0.3rem 0.4rem;
    gap: 0.4rem;
  }
  .reveal-show-as-page {
    font-size: 0.78rem;
    padding: 0.3rem 0.5rem;
  }
}

/* Reduced motion + modern reading mode: collapse all reveal transitions
   to instant. The reveal JS module already bypasses on these modes, but
   defense-in-depth keeps any timing-sensitive CSS quick. */
@media (prefers-reduced-motion: reduce) {
  .chronicle-page.chronicle-revealing,
  .post-chronicle-controls {
    transition-duration: 100ms !important;
  }
  .reveal-playpause {
    transition-duration: 0ms !important;
  }
}

[data-reading-mode="modern"] .chronicle-page.chronicle-revealing,
[data-reading-mode="modern"] .post-chronicle-controls {
  transition-duration: 100ms !important;
}


/* ─────────── F#8 — Per-villager handwriting + drop cap ─────────── */

/* Each villager has a deterministic handwriting key set at creation
   (engine.js → newRun()) and applied as a CSS class on:
   - The standout-attributed chronicle paragraph (renderChronicleParagraphs)
   - The Day 7 meal monologue (renderMealOutcome)
   Per F8_decision: handwriting is identity (frozen at creation); the kind
   may mutate via sigil-change templates but the hand stays put. */

.hand-caveat .meal-monologue,
.meal-outcome.hand-caveat .meal-monologue,
.villager-hand.hand-caveat,
.chronicle-paragraph-with-portrait.hand-caveat {
  font-family: 'Caveat', 'Bradley Hand', cursive;
  font-size: 1.18em;     /* Caveat reads small at body size */
  line-height: 1.55;
}

.hand-tangerine .meal-monologue,
.meal-outcome.hand-tangerine .meal-monologue,
.villager-hand.hand-tangerine,
.chronicle-paragraph-with-portrait.hand-tangerine {
  font-family: 'Tangerine', 'Apple Chancery', cursive;
  font-size: 1.5em;      /* Tangerine is wedding-thin; bump for legibility */
  line-height: 1.4;
}

.hand-blackletter .meal-monologue,
.meal-outcome.hand-blackletter .meal-monologue,
.villager-hand.hand-blackletter,
.chronicle-paragraph-with-portrait.hand-blackletter {
  font-family: 'UnifrakturMaguntia', 'Old English Text MT', serif;
  font-size: 1.12em;
  line-height: 1.55;
  letter-spacing: 0.01em;
}

.hand-reenie .meal-monologue,
.meal-outcome.hand-reenie .meal-monologue,
.villager-hand.hand-reenie,
.chronicle-paragraph-with-portrait.hand-reenie {
  font-family: 'Reenie Beanie', 'Comic Sans MS', cursive;
  font-size: 1.2em;
  line-height: 1.5;
}

/* Per-villager drop caps in standout paragraphs. Sit at 2-line height
   (smaller than the chronicler's 3-line P1 drop cap), colored by archetype
   (color is a typographic affordance, not a font feature — survives modern
   reading mode per F8_decision dissent research on WCAG 1.4.1). */

.chronicle-paragraph-with-portrait .dropcap.dropcap-villager {
  float: left;
  font-family: 'IM Fell English', Georgia, serif;
  font-size: 2.4em;
  line-height: 0.95;
  padding: 0.06em 0.08em 0 0;
  margin-right: 0.1em;
  font-weight: 600;
}

.dropcap.dropcap-villager.dropcap-killer    { color: hsl(0 50% 35%); }
.dropcap.dropcap-villager.dropcap-mourner   { color: hsl(220 25% 22%); }
.dropcap.dropcap-villager.dropcap-wanderer  { color: hsl(30 40% 40%); }
.dropcap.dropcap-villager.dropcap-giver     { color: hsl(35 55% 38%); }
.dropcap.dropcap-villager.dropcap-heretic   { color: hsl(40 12% 50%); }
.dropcap.dropcap-villager.dropcap-survivor  { color: hsl(var(--ink)); }
.dropcap.dropcap-villager.dropcap-reverent  { color: hsl(220 25% 22%); }

/* Modern reading mode: handwriting fonts swap to IBM Plex Sans for
   maximum legibility, but drop cap colors STAY (per F8_decision dissent
   research — color is an affordance, not a font feature; survives WCAG
   1.4.1 because it's not the only signal). */
[data-reading-mode="modern"] .villager-hand.hand-caveat,
[data-reading-mode="modern"] .villager-hand.hand-tangerine,
[data-reading-mode="modern"] .villager-hand.hand-blackletter,
[data-reading-mode="modern"] .villager-hand.hand-reenie,
[data-reading-mode="modern"] .chronicle-paragraph-with-portrait.hand-caveat,
[data-reading-mode="modern"] .chronicle-paragraph-with-portrait.hand-tangerine,
[data-reading-mode="modern"] .chronicle-paragraph-with-portrait.hand-blackletter,
[data-reading-mode="modern"] .chronicle-paragraph-with-portrait.hand-reenie,
[data-reading-mode="modern"] .meal-outcome.hand-caveat .meal-monologue,
[data-reading-mode="modern"] .meal-outcome.hand-tangerine .meal-monologue,
[data-reading-mode="modern"] .meal-outcome.hand-blackletter .meal-monologue,
[data-reading-mode="modern"] .meal-outcome.hand-reenie .meal-monologue {
  font-family: 'IBM Plex Sans', system-ui, sans-serif;
  font-size: inherit;
  line-height: inherit;
  letter-spacing: 0;
}

/* AA-contrast safety: pale-ash (heretic) drop cap falls back to standard
   ink in modern mode where the lighter background of Plex Sans body could
   reduce its 4.5:1 contrast threshold. */
[data-reading-mode="modern"] .dropcap.dropcap-villager.dropcap-heretic {
  color: hsl(var(--ink) / 0.85);
}

/* Mobile: per-font minimum-size floors (Opus missed-risk: Reenie Beanie
   and UnifrakturMaguntia become illegible at small sizes). */
@media (max-width: 480px) {
  .hand-reenie .meal-monologue,
  .meal-outcome.hand-reenie .meal-monologue,
  .villager-hand.hand-reenie,
  .chronicle-paragraph-with-portrait.hand-reenie {
    font-size: 1.12em;
    line-height: 1.55;
  }
  .hand-blackletter .meal-monologue,
  .meal-outcome.hand-blackletter .meal-monologue,
  .villager-hand.hand-blackletter,
  .chronicle-paragraph-with-portrait.hand-blackletter {
    font-size: 1.08em;
    letter-spacing: 0.005em;
  }
}
