/* style.css — BRAND base for The Friction (v20)
 *
 * Defines the docked DOM's structural styling, state classes, and ceremony
 * timing. Per-game CSS lives in style_work.css and overrides via CSS
 * variables and decorative additions. Composers MUST NOT override the state
 * classes (fusion-mode, mode-touch, etc.) or the dynamic insertion order.
 */

/* ============================================================================
   CSS VARIABLES — overridable by 03_page.json.css_vars
   ============================================================================ */

:root {
  /* Generic surface / typography (composer overrides per medium) */
  --bg: #f7f3eb;
  --fg: #1f1a15;
  --muted: rgba(31, 26, 21, 0.55);
  --rule: rgba(31, 26, 21, 0.15);
  --accent: #8e2925;

  --font-primary: Georgia, 'Times New Roman', serif;
  --font-display: 'Helvetica Neue', Arial, sans-serif;
  --font-hand: 'Caveat', cursive;

  /* Tier colors — Aura system. BRAND-FIXED: not overridable from
     03_page.json.css_vars (see CONTRACT §8). The verdict signature is
     the same across every work; the work styles its own atmosphere
     (--bg, --fg, --accent, fonts, decorative chrome) but the moment of
     the verdict is the brand mark.

     Three families:
     - Blues = frictions positives (mind territory: LOW/ZERO/FLOW)
     - Teal  = FULL VIBES (liminal, refreshing — the NPC welcomes)
     - Reds  = HIGH retry + NEG rupture (body territory) */
  --tier-flow-color: #2a3d9e;       /* friction aura=6, mystic electric blue, transcendent */
  --tier-zero-color: #2a9dc7;       /* friction aura=5, bright azure, settled */
  --tier-low-color: #6a8db8;        /* friction aura=3-4, pale slate blue, gentle reach */
  --tier-fullvibes-color: #2a9d8f;  /* touch vibes=3, refreshing teal */
  --tier-high-color: #d96b56;       /* friction high (retry), soft tranquil coral */
  --tier-neg-color: #8b2c28;        /* aura ≤ -3, hard granat, rupture */
  --tier-neutral-color: var(--muted);

  /* Layout */
  --feed-max-width: 720px;
  --feed-padding: 2rem;
  --beat-spacing: 1.6rem;
  /* Tight seam between a narration beat's prose and the interact panel that
     follows it. The page should feel continuous when the textarea attaches
     under the previous text, not band-broken. */
  --interact-attach-ms: 240ms;

  /* Timing — fade is intentionally fast so fusion arrives crisply when the
     judge returns: the user feels the screen go white, not melt. */
  --reveal-fade-ms: 600ms;
  --tier-tint-ms: 600ms;
}

/* ============================================================================
   RESET / BASE
   ============================================================================ */

* { box-sizing: border-box; }

html, body {
  margin: 0;
  padding: 0;
  background: var(--bg);
  color: var(--fg);
  font-family: var(--font-primary);
  font-size: 17px;
  line-height: 1.55;
  -webkit-font-smoothing: antialiased;
  min-height: 100vh;
  overflow-x: hidden;
}

button {
  font: inherit;
  cursor: pointer;
  background: none;
  border: 1px solid var(--rule);
  padding: 0.5em 1em;
  color: var(--fg);
  border-radius: 4px;
  transition: background 0.15s, border-color 0.15s;
}
button:hover { border-color: var(--accent); }
button:disabled { opacity: 0.4; cursor: not-allowed; }

textarea {
  font: inherit;
  background: transparent;
  border: 1px solid var(--rule);
  border-radius: 4px;
  padding: 0.6em 0.8em;
  color: var(--fg);
  resize: vertical;
  min-height: 60px;
  width: 100%;
}
textarea:focus { outline: none; border-color: var(--accent); }

a { color: var(--accent); text-decoration: none; }
a:hover { text-decoration: underline; }

img { max-width: 100%; display: block; }

[hidden] { display: none !important; }

/* ============================================================================
   TITLE SCREEN
   ============================================================================ */

#title-screen {
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  padding: 3rem 2rem;
  position: relative;
}

.title-content { text-align: center; max-width: 700px; }

.title-controls {
  margin-top: 3rem;
  display: flex;
  flex-direction: column;
  gap: 1.5rem;
  align-items: center;
}

.language-selector,
.mode-selector,
.model-selector {
  display: flex;
  gap: 0.5rem;
}

.language-selector button,
.mode-selector button,
.model-selector button {
  text-transform: uppercase;
  letter-spacing: 0.05em;
  font-size: 0.85em;
}

.language-selector button.selected,
.mode-selector button.selected,
.model-selector button.selected {
  border-color: var(--accent);
  color: var(--accent);
}

/* Model selector buttons are two-line: name + small descriptor. */
.model-selector button {
  display: inline-flex;
  flex-direction: column;
  align-items: center;
  gap: 2px;
  padding: 0.5em 1em;
}
.model-selector .model-name {
  font-weight: 600;
}
.model-selector .model-desc {
  font-size: 0.78em;
  text-transform: none;
  letter-spacing: 0.02em;
  opacity: 0.75;
}

.begin-btn {
  font-size: 1.1em;
  padding: 0.7em 2em;
  letter-spacing: 0.05em;
}

.brand-mark {
  position: absolute;
  bottom: 1.5rem;
  right: 1.5rem;
  font-family: var(--font-display);
  font-size: 0.75em;
  letter-spacing: 0.15em;
  color: var(--muted);
  text-transform: uppercase;
}

/* ============================================================================
   STAGE
   ============================================================================ */

#stage {
  display: flex;
  flex-direction: column;
  min-height: 100vh;
  /* BRAND breathing room: empty page space BELOW the last meaningful child
     of #stage (the latest beat in #feed when no interaction is open, or
     the open #interact panel when there is). The padding lives on the
     stage rather than on #feed so #interact attaches flush under the last
     beat — the breathing room is always at the END of the column, never
     between the prose and the textarea. !important so per-game composer
     CSS cannot collapse it. */
  padding-bottom: 50vh !important;
}

/* Header — ALWAYS visible (fixed to top, never hidden by scroll regardless
   of overflow / backdrop-filter / grid quirks). Three columns: title +
   author + mode + protagonist (left), lives + progress (center), brand +
   settings (right). Composer's CSS may restyle the visual; the position
   and structure are BRAND. */
#header {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  z-index: 50;
  background: var(--bg);
  display: grid;
  grid-template-columns: minmax(0, 1.2fr) minmax(0, 1.5fr) minmax(0, 1.2fr);
  align-items: center;
  gap: 1.5rem;
  padding: 0.75rem 1.5rem;
  border-bottom: 1px solid var(--rule);
}

/* Reserve vertical space for the fixed header so #feed and #interact don't
   slide underneath. Composers can override this height per work in their
   own CSS; the default reserves ~58px. */
#stage { padding-top: 58px; }

.header-left {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
}
.header-title {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: 13px;
  letter-spacing: 0.05em;
  color: var(--fg);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.header-byline {
  font-size: 10px;
  color: var(--muted);
  letter-spacing: 0.18em;
  text-transform: uppercase;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.header-byline .header-year::before { content: ' · '; opacity: 0.5; margin: 0 4px; }
.header-byline .header-year:empty::before { content: ''; }

.header-center {
  display: flex;
  align-items: center;
  gap: 0.8rem;
  min-width: 0;
}

.header-right {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: 0.8rem;
}

.header-brand-mark {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: 9px;
  letter-spacing: 0.36em;
  text-transform: uppercase;
  color: var(--muted);
  white-space: nowrap;
}

#hearts {
  display: flex;
  gap: 0.3rem;
  font-size: 1.1em;
  color: var(--accent);
}
.heart { transition: opacity 0.4s; }
.heart.lost { opacity: 0.2; color: var(--muted); }

/* Casual heart — rendered entirely via CSS pseudo-elements so no textContent
   is needed in JS. ::before is the heart, ::after is a small infinity that
   tells the player "lives are uncapped". */
.heart.heart-infinite {
  display: inline-flex;
  align-items: center;
  gap: 3px;
  color: var(--accent);
  opacity: 0.9;
  font-size: 1em;
  line-height: 1;
}
.heart.heart-infinite::before { content: '\2665'; }
.heart.heart-infinite::after {
  content: '\221E';
  font-size: 0.7em;
  font-weight: 700;
  margin-top: -1px;
}

/* Casual mode: a single always-full heart is rendered in JS — no display:none */

#progress-bar {
  flex: 1;
  margin: 0 1.5rem;
  height: 2px;
  background: var(--rule);
  position: relative;
  border-radius: 1px;
}
#progress-bar::after {
  content: '';
  position: absolute;
  left: 0; top: 0; bottom: 0;
  width: var(--progress, 0%);
  background: var(--accent);
  transition: width 0.5s ease;
}

#settings-toggle {
  border: none;
  font-size: 1.2em;
  padding: 0.2em 0.4em;
  color: var(--muted);
}
#settings-toggle:hover { color: var(--fg); border: none; }

/* ============================================================================
   FEED + BEATS
   ============================================================================ */

#feed {
  flex: 1;
  width: 100%;
  max-width: var(--feed-max-width);
  margin: 0 auto;
  padding: var(--feed-padding);
}

.beat {
  margin-bottom: var(--beat-spacing);
  position: relative;
  transition: opacity var(--reveal-fade-ms) ease;
}

/* Scroll-buffer (BRAND): a small gap between a resolved interactive beat
   and the next beat — enough to mark the seam, not so much that the page
   feels broken. Earlier versions used 38vh here as a scroll affordance;
   the gap was too large and read as empty space. The new flow keeps the
   page continuous: prose flows tight after an interaction. */
#feed .beat.resolved + .beat {
  padding-top: 0.6rem;
}

/* Tight seam to the interact panel: the last beat in #feed trims its bottom
   margin so the textarea (sibling of #feed in the DOM) lands flush under
   the prose. Combined with #interact's negative top margin, the panel reads
   as a continuation of the beat above, not a separate band. */
#feed > .beat:last-child {
  margin-bottom: 0.3rem;
}

.beat .speaker-label {
  font-family: var(--font-display);
  font-size: 0.78em;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--muted);
  margin-bottom: 0.4em;
}

/* Interactive beats no longer carry a beat-top .speaker-label: each rebound
   self-titles (player / npc / author) inside its own box. Doubling the
   character name above and below was redundant. The runtime stops emitting
   the label on touch/friction beats; this rule is a safety net for any
   composer who hand-builds DOM. Narration interludes keep the label. */
.beat[data-role="touch"] > .speaker-label,
.beat[data-role="friction"] > .speaker-label {
  display: none !important;
}

.beat .body { white-space: pre-wrap; }

/* Rebounds — BRAND-LOCKED chrome.
 *
 * Older builds painted each rebound with a colored left-border bar (red for
 * player, fg for author, muted for npc). The pattern was too visually
 * obvious — every game looked the same and the bot kept producing it.
 * The new BRAND chrome is a subtle background rectangle, no border. The
 * box is felt, not announced. Tinting per character still happens via the
 * data-speaker attribute on the .beat block (see CONTRACT §3) — but the
 * rebound's outer frame is identical across works.
 *
 * The chrome-defining properties are flagged !important so per-game
 * style_work.css cannot reintroduce a left bar. Composer can still tint
 * the title/text colors and add decorative pseudo-elements if needed.
 */
.beat .rebound {
  margin-top: 0.7em;
  padding: 0.65em 0.9em !important;
  border: none !important;
  border-radius: 2px;
  /* Default subtle tint. Per-game CSS may override per data-speaker
     (e.g. tinting Carme's rebounds with the manteca hue, the senyora's
     with rubric, etc.) — see CONTRACT §3 on the data-speaker attribute. */
  background: color-mix(in srgb, var(--fg) 3%, transparent);
}
.beat .rebound + .rebound { margin-top: 0.55em; }

/* Rebound title shares the SAME visual contract as narration .dialogue-title
   so every voiced line on the page reads at one typographic rhythm. The
   text-transform stays NEUTRAL — display names like "la Carme" already
   carry their proper-noun casing, and a forced uppercase erased that
   distinction. Composer per-game CSS may add its own treatment (italics,
   colour, slight tracking) but should leave the case alone. */
.beat .rebound .rebound-title,
.beat .body .dialogue-title {
  font-family: var(--font-display);
  font-size: 0.78em;
  letter-spacing: 0.04em;
  text-transform: none;
  color: var(--muted);
  margin-bottom: 0.35em;
  font-weight: 600;
}
.beat .rebound .rebound-text { white-space: pre-wrap; }

.beat .rebound.player .rebound-text { font-style: italic; }
.beat .rebound.npc .rebound-text {
  font-family: var(--font-display);
  font-size: 0.95em;
}
.beat .rebound.author { margin-top: 1em; }

/* Narration segments: prose paragraphs + dialogue boxes interleaved.
   Prose flows in normal body text; dialogue boxes carry the character's
   name above and the spoken line below (same visual contract as .rebound
   for interactive beats — both are "voiced lines" rendered as titled
   boxes). Dialogue text is dropped without em-dashes by the composer; the
   format alone marks it as dialogue. */
.beat .body .prose {
  margin: 0 0 0.6em;
  font-family: var(--font-primary);
  font-size: 1em;
  line-height: 1.6;
  white-space: pre-wrap;
}
.beat .body .prose:last-child { margin-bottom: 0; }

/* Narration dialogue boxes — BRAND-LOCKED chrome (same rule as .rebound).
   Border + padding are locked; background defaults to a subtle muted tint
   but per-game CSS may shift the hue toward the speaker's palette. */
.beat .body .dialogue {
  margin: 0.7em 0;
  padding: 0.65em 0.9em !important;
  border: none !important;
  border-radius: 2px;
  background: color-mix(in srgb, var(--fg) 3%, transparent);
}
.beat .body .dialogue-text {
  font-family: var(--font-primary);
  font-size: 1em;
  line-height: 1.6;
  white-space: pre-wrap;
}

/* Verdict + score + cryst land together as a single inline row at fusion exit */
.beat .verdict-row {
  margin-top: 0.9em;
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.7em;
  font-family: var(--font-display);
  font-size: 0.78em;
  line-height: 1.4;
}

/* The verdict word (FLOW STATE / ZERO FRICTION / LOW FRICTION / HIGH FRICTION
   / -N AURA / FULL VIBES) is the heaviest mark of the row — uppercase,
   slightly larger, bold. The reader feels which tier landed before reading
   the score numbers. tierLabel() always emits uppercase BRAND English. */
.beat .verdict-row .verdict {
  margin: 0;
  font-family: var(--font-display);
  font-size: 1.35em;
  font-weight: 700;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  line-height: 1.2;
}

/* HIGH FRICTION pulses in sync with the heartbeat (the audible low pulse
   the reader hears between attempts). Runtime sets `--verdict-pulse-ms`
   on the .verdict.high element to match the heartbeat interval for the
   next attempt: 800ms at attempt 2, 420ms at attempt 3. The default
   1600ms matches intent-1 cadence. */
.beat .verdict-row .verdict.high {
  animation: verdict-pulse var(--verdict-pulse-ms, 1600ms) ease-in-out infinite;
}
@keyframes verdict-pulse {
  0%, 100% { opacity: 1; transform: scale(1); }
  45%      { opacity: 0.55; transform: scale(1.06); }
  55%      { opacity: 0.55; transform: scale(1.06); }
}
/* Once the beat resolves to a non-high outcome (low / zero / flow / -X aura),
   any prior .verdict.high blocks in the same beat must STOP pulsing — the
   moment is closed; a frozen "HIGH FRICTION" mark is the right echo. */
.beat.resolved .verdict-row .verdict.high {
  animation: none !important;
  opacity: 0.7;
  transform: none;
}

.beat .verdict-row .score-line {
  margin: 0;
  font-family: inherit;
  font-size: 0.95em;
  color: var(--muted);
  letter-spacing: 0.05em;
}

.beat .verdict-row .cryst {
  margin: 0;
  font-family: var(--font-hand), var(--font-primary);
  font-size: 1.10em;
  color: var(--muted);
}

/* Legacy stacked layout (kept for non-friction touches that may still stack) */
.beat .verdict {
  margin-top: 0.8em;
  font-family: var(--font-display);
  font-size: 0.78em;
  letter-spacing: 0.12em;
  text-transform: uppercase;
}
.verdict.flow         { color: var(--tier-flow-color); }
.verdict.zero         { color: var(--tier-zero-color); }
.verdict.low          { color: var(--tier-low-color); }
.verdict.high         { color: var(--tier-high-color); }
.verdict.neg-aura,
.verdict.neg          { color: var(--tier-neg-color); }
.verdict.full-vibes   { color: var(--tier-fullvibes-color); }
.verdict.neutral      { color: var(--tier-neutral-color); }

.beat .score-line {
  margin-top: 0.3em;
  font-family: var(--font-display);
  font-size: 0.78em;
  color: var(--muted);
  letter-spacing: 0.05em;
}

.beat .cryst {
  margin-top: 0.4em;
  font-family: var(--font-hand), var(--font-primary);
  font-size: 1.05em;
  color: var(--muted);
}

.beat .alt-ending {
  margin-top: 1.2em;
  padding-left: 1em;
  border-left: 1px dashed var(--rule);
  font-style: italic;
  color: var(--muted);
}

/* Climax beats can be styled by composer via .beat[data-climax="true"] */

/* Resolved beats keep the .resolved + .resolved-{tier} classes (composer CSS
   may target them for atmospheric chrome, e.g. per-character tints), but
   BRAND no longer paints the block with a tier-tinted gradient. The verdict
   word IS the color; the block stays clean. */

/* ============================================================================
   INTERACT PANEL
   ============================================================================ */

#interact {
  /* Position: NORMAL FLOW (relative). Tight attach: the textarea reads as
     part of the previous beat's body — there should be no empty band
     between the prose above and the panel below. The negative top margin
     pulls #interact up tight under the last .beat, which itself uses
     --beat-spacing as its bottom margin. */
  position: relative;
  width: 100%;
  max-width: var(--feed-max-width);
  margin: -0.4rem auto 0.8rem auto;
  padding: 0.2rem 0;
  z-index: 5;
  transition: transform var(--interact-attach-ms) ease, opacity var(--interact-attach-ms) ease;

  /* Layout (BRAND): textarea on top, controls/reveal below. Touches and
     frictions share the same vertical structure — write OR reveal. The
     order is enforced via flex `order` so it doesn't depend on DOM order. */
  display: flex;
  flex-direction: column;
  align-items: stretch;
}

#interact textarea         { margin-bottom: 0.6em; }

/* Role tag at the top of the interact panel:
     <span class="role-name">TOUCH</span><span class="role-explain"> (inhabita la línia)</span>
   - .role-name is BRAND English (TOUCH / FRICTION), stable across languages.
   - .role-explain is a localized parenthetical explanation in the chosen
     language (brand_strings.role_touch_explain / role_friction_explain).
     Composer can leave it empty to suppress; the BRAND name is the anchor. */
#interact .interact-role {
  font-family: var(--font-display);
  font-size: 0.7em;
  color: var(--muted);
  margin-bottom: 0.4em;
  text-align: center;
}
#interact .interact-role .role-name {
  letter-spacing: 0.18em;
  text-transform: uppercase;
  font-weight: 600;
}
#interact .interact-role .role-explain {
  letter-spacing: 0.04em;
  text-transform: none;
  font-style: italic;
  opacity: 0.85;
}

#interact .role-reminder {
  font-family: var(--font-display);
  font-size: 0.78em;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--muted);
  margin-bottom: 0.6em;
  text-align: center;
}

/* .accept-original (touch) and .reveal-btn (friction) are siblings of
   .submit inside .interact-controls. Only one is visible at a time
   (touches show .accept-original; frictions show .reveal-btn). They
   share the same visual treatment: small, italic, dashed border —
   parallel to .submit which is solid + accent. The runtime sets
   .accept-original's label to brand_strings.show_line so it matches
   .reveal-btn's label too. */
#interact .accept-original,
#interact .reveal-btn {
  font-style: italic;
  color: var(--muted);
  border-style: dashed;
}
#interact .accept-original:hover,
#interact .reveal-btn:hover {
  color: var(--fg);
}

#interact textarea {
  width: 100%;
  max-width: var(--feed-max-width);
  margin: 0 auto;
  display: block;
}

#interact .interact-controls {
  margin-top: 0.6em;
  display: flex;
  gap: 0.8em;
  align-items: center;
  justify-content: center;
}

#interact .or-divider {
  font-family: var(--font-display);
  font-size: 0.78em;
  color: var(--muted);
}

#interact .submit {
  background: var(--accent);
  color: white;
  border-color: var(--accent);
}
#interact .submit:hover {
  filter: brightness(1.1);
}

#interact .reveal-btn {
  font-style: italic;
  color: var(--muted);
  border-style: dashed;
}

#interact .attempt-counter {
  text-align: center;
  font-family: var(--font-display);
  font-size: 0.75em;
  color: var(--muted);
  margin-top: 0.5em;
  letter-spacing: 0.1em;
}

/* The thinking-banner is fixed at the viewport bottom. With #feed's
   60vh bottom padding (above), the latest rebound is scrolled to the
   visual center, well above where the banner sits — no extra padding
   needed when body.thinking is on. */

/* Thinking banner — appears at the bottom of the viewport (fixed) while the
   judge call is in flight, after #interact has been hidden. Composer can
   restyle the icon and text but the position is BRAND (always fixed at the
   bottom, centered). */
#thinking-banner {
  position: fixed;
  left: 50%;
  bottom: 24px;
  transform: translateX(-50%);
  z-index: 40;
  display: inline-flex;
  align-items: center;
  gap: 0.7em;
  padding: 12px 20px;
  background: var(--bg);
  border: 1px solid var(--rule);
  font-family: var(--font-display);
  font-size: 12px;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--muted);
  pointer-events: none;
}
#thinking-banner .thinking-text { white-space: nowrap; }
#thinking-banner .thinking-icon {
  display: inline-block;
  width: 14px; height: 14px;
  border: 1.5px solid var(--accent);
  border-top-color: transparent;
  border-radius: 50%;
  animation: thinking-spin 0.9s linear infinite;
}
@keyframes thinking-spin {
  0%   { transform: rotate(0deg); }
  100% { transform: rotate(360deg); }
}

/* Legacy inline-spinner kept for back-compat (composer CSS may still target it) */
#interact .inline-spinner {
  text-align: center;
  margin-top: 0.6em;
  color: var(--muted);
  font-size: 0.85em;
}
#interact .inline-spinner::after {
  content: '...';
  animation: spinner-dots 1.4s infinite;
}
@keyframes spinner-dots {
  0%, 20%   { content: '.'; }
  40%       { content: '..'; }
  60%, 100% { content: '...'; }
}

/* ============================================================================
   STATE BODY CLASSES
   ============================================================================ */

/* Fusion mode — the SOLEMN reveal:
   - The page background goes WHITE (overrides any composer atmosphere).
   - All chrome disappears: header, interact, every prior beat, and even
     the last beat's chrome go to opacity 0.
   - The two reaches that remain (.rebound.player + .rebound.author of
     the last beat) lose their borders, backgrounds, padding, and titles
     — what remains is BLACK TEXT ON WHITE, two stretches of prose.
   - Audio: the runtime suspends the bed and any in-flight motif; the
     only sound the reader hears during fusion is the ceremony at the
     end. (See runtime.js Audio.suspend / Audio.resume.)
   The contrast between this silent white page and the ceremony arrival
   gives the verdict its weight. The composer's CSS must also dim its own
   decorative pseudo-elements (background gradients, ::before / ::after
   on body, on title-screen, etc.) to opacity 0 under body.fusion-mode. */
body.fusion-mode {
  background: #ffffff !important;
  transition: background var(--reveal-fade-ms) ease;
}

/* Pre-verdict flash — fixed-position overlay layered over the white fusion
   page. The runtime fades it in/out twice (double blink) just before the
   page returns to the work's atmosphere. Subtle by design: low opacity
   (~0.4) so the colour washes the page rather than replacing it; quick
   60ms transitions so each blink is crisp. The overlay sits above all
   content (z-index 9999) and is pointer-events: none so it never
   interferes with input. */
#verdict-flash {
  position: fixed;
  inset: 0;
  background: var(--pre-verdict-color, transparent);
  opacity: 0;
  pointer-events: none;
  z-index: 9999;
  transition: opacity 60ms ease-out;
}
body.pre-verdict-flash #verdict-flash {
  opacity: 0.40;
}
body.fusion-mode #header,
body.fusion-mode #interact {
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--reveal-fade-ms) ease;
}
body.fusion-mode .beat:not(:last-child) {
  opacity: 0;
  transition: opacity var(--reveal-fade-ms) ease;
}
body.fusion-mode .beat:last-child > *:not(.rebound.player):not(.rebound.author) {
  opacity: 0;
  transition: opacity var(--reveal-fade-ms) ease;
}
/* Strip ALL chrome from the last beat during fusion: no border (per-game
   CSS can add a red top/bottom rule on touch/friction beats — kill those
   too), no background, no padding/margin around the beat, no resolved
   tier tints. The white must be total: only black text, centered. The
   chrome-defining rules are flagged !important so per-game style_work.css
   cannot reintroduce them inside fusion. */
body.fusion-mode .beat:last-child {
  background: none !important;
  border: none !important;
  padding: 0 !important;
  margin: 0 !important;
}
body.fusion-mode .beat:last-child .rebound.player,
body.fusion-mode .beat:last-child .rebound.author {
  border: none !important;
  background: none !important;
  padding: 1.6rem 0 !important;
  margin: 0 !important;
  color: #111;
  text-align: center;
  font-style: normal;
  font-family: var(--font-primary);
  font-size: 1.05em;
  line-height: 1.7;
  transition: opacity var(--reveal-fade-ms) ease;
}
body.fusion-mode .beat:last-child .rebound .rebound-title {
  display: none;
}
body.fusion-mode .beat:last-child .rebound.player .rebound-text,
body.fusion-mode .beat:last-child .rebound.author .rebound-text {
  font-style: normal;
  font-family: var(--font-primary);
}

/* Hide everything that isn't the LAST player rebound (.fusion-final) or
   the author rebound (.rebound.author, appended later by fusionReveal):
   prior high-friction attempts' .rebound.player blocks AND every
   .rebound.npc (the in-attempt NPC hints) disappear so the white page
   carries one reach + one author line, nothing else. */
body.fusion-mode .beat:last-child .rebound.npc,
body.fusion-mode .beat:last-child .rebound.player:not(.fusion-final) {
  display: none !important;
}

/* Audio-off / graphic-off */
body.audio-off { /* No visual effect; the runtime mutes audio */ }
body.graphic-off .beat .verdict,
body.graphic-off .beat .score-line,
body.graphic-off .beat .cryst { /* keep visible — they're informational */ }
body.graphic-off .beat.resolved { background: none; }

/* ============================================================================
   POST-GAME (3-column grid, no scroll)
   ============================================================================ */

#post-game {
  display: grid;
  grid-template-columns: 22% 1fr 22%;
  gap: 2rem;
  padding: 2rem;
  min-height: 100vh;
  max-height: 100vh;
  overflow: hidden;
}

.pg-sidebar,
.pg-main,
.pg-feedback {
  display: flex;
  flex-direction: column;
  gap: 0.55rem;
  overflow: hidden;
}

.pg-sidebar { border-right: 1px solid var(--rule); padding-right: 2rem; }
.pg-feedback { border-left: 1px solid var(--rule); padding-left: 2rem; }

/* BRAND: bigger author photo, tighter sidebar block. The bio sits flush
   under the meta so the column reads as one card and rarely needs to
   scroll. */
.pg-author-photo {
  width: 100%;
  aspect-ratio: 1;
  object-fit: cover;
  border-radius: 4px;
  background: var(--rule);
  margin-bottom: 0.4rem;
}
.pg-author-name {
  font-family: var(--font-display);
  font-size: 1.15em;
  font-weight: 600;
  line-height: 1.15;
}
.pg-author-meta {
  font-size: 0.82em;
  color: var(--muted);
  margin-bottom: 0.15rem;
}
.pg-author-bio {
  font-size: 0.9em;
  line-height: 1.5;
  flex: 1;
  overflow-y: auto;
  margin-top: 0.15rem;
}
.pg-source {
  font-size: 0.8em;
  color: var(--accent);
}

.pg-header { margin-bottom: 0.5rem; }
.pg-title { font-size: 1.6em; font-weight: 600; }
.pg-subtitle { color: var(--muted); font-style: italic; }

/* Scoreboard — scores left, 2-row 4-col stats grid right. The stats grid
   gets the wider portion so labels can wrap to two lines. */
.pg-scoreboard {
  display: grid;
  grid-template-columns: minmax(0, 0.65fr) minmax(0, 1.95fr);
  gap: 1.2rem;
  padding: 0.8rem 0;
  border-top: 1px solid var(--accent);
  border-bottom: 1px solid var(--rule);
  align-items: stretch;
}

.pg-scores {
  display: flex;
  flex-direction: column;
  gap: 0.7rem;
  justify-content: center;
}
.pg-score-value {
  font-family: var(--font-display);
  font-size: 2em;
  font-weight: 700;
  color: var(--accent);
  line-height: 1;
}
.pg-score-detail {
  font-family: var(--font-display);
  font-size: 0.7em;
  font-weight: 600;
  color: var(--muted);
  margin-top: 0.2em;
}
.pg-score-label {
  font-family: var(--font-display);
  font-size: 0.65em;
  letter-spacing: 0.3em;
  text-transform: uppercase;
  color: var(--muted);
  margin-top: 0.25em;
}

.pg-stats-grid {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  grid-auto-rows: minmax(0, 1fr);
  gap: 0.3rem;
}
.pg-stats-grid .stat {
  padding: 0.45em 0.6em;
  background: color-mix(in srgb, var(--fg) 4%, transparent);
  border-radius: 2px;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
}
.pg-stats-grid .stat-value {
  font-family: var(--font-display);
  font-size: 0.92em;
  font-weight: 700;
  line-height: 1.1;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
/* Stat labels wrap to 2 lines without breaking words. */
.pg-stats-grid .stat-label {
  font-family: var(--font-display);
  color: var(--muted);
  font-size: 0.55em;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  margin-top: 0.25em;
  line-height: 1.25;
  white-space: normal;
  word-break: keep-all;
  overflow-wrap: normal;
  hyphens: none;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

/* Best moments — clean horizontal pair, no separators on either side. The
   block is short and wide so the reflection below can take the vertical
   weight. Composer per-game CSS may add a subtle background tint but should
   not reintroduce vertical bars. */
.pg-best-moments {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 0.7rem;
  font-size: 0.84em;
  flex-shrink: 0;
}
.pg-best-touch,
.pg-best-friction {
  padding: 0.5em 0.6em;
  border: none;
  line-height: 1.4;
}
.pg-best-touch .label,
.pg-best-friction .label {
  font-family: var(--font-display);
  font-size: 0.7em;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--muted);
  margin-bottom: 0.25em;
}
.pg-best-touch .player,
.pg-best-friction .player {
  font-style: italic;
}
.pg-best-touch .author,
.pg-best-friction .author {
  margin-top: 0.25em;
  color: var(--accent);
}

/* Reflection ("Una mirada") — the gravity center of the post-game. Author's
   note about the player. Should feel substantial: at least as much vertical
   weight as best moments + recommendations combined. flex-grow takes the
   leftover space in the middle column. */
.pg-reflection {
  padding: 1.1em 1.2em;
  background: color-mix(in srgb, var(--fg) 4%, transparent);
  border-radius: 4px;
  font-style: italic;
  font-size: 1em;
  line-height: 1.6;
  flex: 1 1 auto;
  min-height: 7.5em;
  overflow-y: auto;
}

.pg-recommendations {
  display: flex;
  flex-direction: column;
  gap: 0.35em;
  font-size: 0.85em;
}

/* Each .rec is a clickable icon card. The user can scan the list, click
   one, and land on Google Books — even when the future "actually open
   the book" backend isn't there yet, the cards feel inviting and read
   as actionable, which is the retention hook. */
.pg-recommendations .rec {
  display: flex;
  align-items: center;
  gap: 0.6em;
  padding: 0.5em 0.65em;
  border: 1px solid var(--rule);
  border-radius: 4px;
  background: transparent;
  color: var(--fg);
  text-decoration: none;
  transition: background 0.15s ease, border-color 0.15s ease;
  min-height: 2.4em;
}
.pg-recommendations .rec:hover {
  background: color-mix(in srgb, var(--accent) 6%, transparent);
  border-color: var(--accent);
  text-decoration: none;
}
.pg-recommendations .rec-icon {
  flex-shrink: 0;
  font-size: 1.1em;
  opacity: 0.7;
  width: 1.4em;
  text-align: center;
}
.pg-recommendations .rec-body {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 0.1em;
  overflow: hidden;
}
.pg-recommendations .rec-head {
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.pg-recommendations .rec-title {
  font-weight: 600;
}
.pg-recommendations .rec-surname {
  color: var(--muted);
}
.pg-recommendations .rec-why {
  color: var(--muted);
  font-size: 0.88em;
  font-style: italic;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.pg-recommendations .rec-arrow {
  flex-shrink: 0;
  opacity: 0;
  color: var(--accent);
  transition: opacity 0.15s ease;
  font-size: 0.9em;
}
.pg-recommendations .rec:hover .rec-arrow { opacity: 1; }

/* Loading placeholders — three grey cards with shimmer-like pulse so the
   reader sees recommendations are coming and the layout doesn't jump. */
.pg-recommendations .rec-placeholder {
  pointer-events: none;
  border-color: var(--rule);
  background: color-mix(in srgb, var(--fg) 3%, transparent);
}
.pg-recommendations .rec-placeholder .rec-icon { opacity: 0.3; }
.pg-recommendations .rec-placeholder .rec-title-line,
.pg-recommendations .rec-placeholder .rec-why-line {
  display: block;
  height: 0.7em;
  border-radius: 2px;
  background: var(--rule);
  animation: pg-loading-pulse 1.6s ease-in-out infinite;
}
.pg-recommendations .rec-placeholder .rec-title-line { width: 60%; margin-bottom: 0.35em; }
.pg-recommendations .rec-placeholder .rec-why-line   { width: 80%; height: 0.55em; }

.pg-feedback-title {
  font-family: var(--font-display);
  font-size: 0.85em;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--muted);
}
/* Survey — rendered visible from the start so the reader can answer while
   the reflection + recommendations are still loading. On submit, .pg-survey
   is hidden and .pg-thanks-state is shown. The column is sized so it never
   needs to scroll: tight gaps, compact star rows, and the comment textarea
   stays minimal. */
.pg-survey { display: flex; flex-direction: column; gap: 0.45rem; }
.pg-stars  { display: flex; flex-direction: column; gap: 0.35em; }

.feedback-row {
  display: flex;
  justify-content: space-between;
  align-items: center;
  font-size: 0.85em;
  gap: 0.8em;
}
.feedback-row label { flex: 1; line-height: 1.3; }

/* Stars: each star is its own clickable span. Initial state = ☆ (empty);
   filled = ★ (after click). The runtime swaps content on click. */
.feedback-row .stars {
  display: inline-flex;
  gap: 0.15em;
  cursor: pointer;
  color: var(--accent);
  font-size: 1em;
  white-space: nowrap;
}
.feedback-row .stars .star {
  color: var(--rule);
  transition: color 0.15s ease;
  cursor: pointer;
}
.feedback-row .stars .star.on { color: var(--accent); }

/* Difficulty — three buttons (too easy / just right / too hard) */
.pg-difficulty { display: flex; flex-direction: column; gap: 0.35em; }
.pg-difficulty-label {
  font-size: 0.85em;
  color: var(--muted);
}
.pg-difficulty-buttons {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 0.3em;
}
.pg-diff-btn {
  font-family: var(--font-display);
  font-size: 0.7em;
  letter-spacing: 0.06em;
  padding: 0.55em 0.4em;
  background: transparent;
  color: var(--fg);
  border: 1px solid var(--rule);
  cursor: pointer;
  text-align: center;
  transition: background 0.15s ease, color 0.15s ease, border-color 0.15s ease;
}
.pg-diff-btn:hover { border-color: var(--accent); }
.pg-diff-btn.selected {
  background: var(--accent);
  color: #fff;
  border-color: var(--accent);
}

.pg-textarea { font-size: 0.9em; min-height: 60px; }

/* Per-call cost log (BRAND). Lives inside #cost-modal — opened from a
   small $ link in .pg-actions. Used to render at the bottom of .pg-main
   but the inline block crowded the post-game card; the modal is now the
   only entry point. The .pg-cost element in .pg-main is hidden as a
   safety net for any older composer CSS that still styles it. */
.pg-main .pg-cost { display: none; }

#cost-modal .pg-cost {
  font-family: var(--font-display);
  font-size: 0.85em;
  color: var(--fg);
}

/* Tiny $ link that lives in the .pg-actions area on the right column.
   Opens #cost-modal. Hidden when there are no recorded calls. */
.pg-cost-link {
  background: transparent;
  border: 1px dashed var(--rule);
  color: var(--muted);
  font-family: var(--font-display);
  font-size: 0.8em;
  letter-spacing: 0.08em;
  padding: 0.4em 0.6em;
  cursor: pointer;
  text-align: left;
}
.pg-cost-link:hover { color: var(--accent); border-color: var(--accent); }
.pg-cost-link .dollar {
  font-weight: 700;
  margin-right: 0.4em;
}

#cost-modal {
  border: none;
  border-radius: 8px;
  padding: 1.6rem 2rem;
  background: var(--bg);
  color: var(--fg);
  box-shadow: 0 12px 40px rgba(0, 0, 0, 0.18);
  max-width: 560px;
  width: 90%;
}
#cost-modal::backdrop { background: rgba(0, 0, 0, 0.4); backdrop-filter: blur(4px); }
#cost-modal .cost-modal-close {
  margin-top: 1rem;
  display: block;
  margin-left: auto;
}

.pg-cost-title {
  font-size: 0.95em;
  text-transform: uppercase;
  letter-spacing: 0.12em;
  margin-bottom: 0.5em;
}
.pg-cost-model-name {
  text-transform: none;
  letter-spacing: normal;
  color: var(--fg);
  font-family: var(--font-primary);
}
.pg-cost-table {
  width: 100%;
  border-collapse: collapse;
  font-variant-numeric: tabular-nums;
}
.pg-cost-table th,
.pg-cost-table td {
  padding: 3px 8px 3px 0;
  text-align: left;
  border-bottom: 1px dotted var(--rule);
  vertical-align: baseline;
}
.pg-cost-table th {
  color: var(--muted);
  font-weight: normal;
  font-size: 0.85em;
  text-transform: uppercase;
  letter-spacing: 0.08em;
}
.pg-cost-cell-amount {
  text-align: right;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
.pg-cost-cell-tokens {
  font-family: var(--font-primary);
  font-size: 0.95em;
}
.pg-cost-cached  { color: var(--tier-flow-color); margin-left: 6px; }
.pg-cost-created { color: var(--tier-high-color); margin-left: 6px; }
.pg-cost-table tfoot td {
  font-weight: bold;
  color: var(--fg);
  border-bottom: none;
  padding-top: 6px;
}
.pg-cost-total-amount {
  color: var(--accent);
}

/* Loading indicator on reflection while the judge call is in flight.
   Pulsing accent text so the reader knows the system is thinking.
   Recommendations have their own placeholder cards (rendered by the
   runtime), so we don't apply the text-replacement style there. */
.pg-reflection.is-loading {
  font-family: var(--font-display);
  font-size: 0.7em;
  letter-spacing: 0.3em;
  text-transform: uppercase;
  color: var(--accent);
  font-style: normal;
  animation: pg-loading-pulse 1.6s ease-in-out infinite;
}
@keyframes pg-loading-pulse {
  0%, 100% { opacity: 0.4; }
  50%      { opacity: 1.0; }
}
.pg-submit {
  background: var(--accent);
  color: white;
  border-color: var(--accent);
}
.pg-thanks-state { color: var(--muted); font-size: 0.9em; font-style: italic; }

.pg-actions {
  margin-top: auto;
  display: flex;
  flex-direction: column;
  gap: 0.4em;
}
.pg-actions button { font-size: 0.85em; }

/* ============================================================================
   SETTINGS MODAL
   ============================================================================ */

#settings-modal,
#gameover-modal {
  border: none;
  border-radius: 8px;
  padding: 2rem;
  background: var(--bg);
  color: var(--fg);
  box-shadow: 0 12px 40px rgba(0, 0, 0, 0.18);
  max-width: 480px;
  width: 90%;
}
#settings-modal::backdrop,
#gameover-modal::backdrop {
  background: rgba(0, 0, 0, 0.4);
  backdrop-filter: blur(4px);
}

#settings-modal h2 {
  margin-top: 0;
  font-family: var(--font-display);
  font-size: 1em;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--muted);
}
#settings-modal label {
  display: flex;
  align-items: center;
  gap: 0.5em;
  margin: 0.8em 0;
  font-size: 0.95em;
}
#settings-modal input[type="range"] { flex: 1; }
#settings-close { margin-top: 1rem; }

/* ============================================================================
   GAME-OVER MODAL
   ============================================================================ */

#gameover-modal {
  text-align: center;
}
.gom-message {
  font-family: var(--font-primary);
  font-size: 1.1em;
  font-style: italic;
  color: var(--fg);
  margin-bottom: 1.5rem;
  line-height: 1.5;
}
.gom-options {
  display: flex;
  flex-direction: column;
  gap: 0.6em;
}
.gom-options button {
  width: 100%;
  padding: 0.8em;
  font-size: 0.95em;
}
.gom-options button.primary {
  background: var(--accent);
  color: white;
  border-color: var(--accent);
}
.gom-options button.primary:hover { filter: brightness(1.1); }

/* ============================================================================
   RESPONSIVE — collapse 3-col post-game on narrow screens
   ============================================================================ */

@media (max-width: 900px) {
  #post-game {
    grid-template-columns: 1fr;
    max-height: none;
    overflow-y: auto;
  }
  .pg-sidebar { border-right: none; border-bottom: 1px solid var(--rule); padding-right: 0; padding-bottom: 1.5rem; }
  .pg-feedback { border-left: none; border-top: 1px solid var(--rule); padding-left: 0; padding-top: 1.5rem; }
  .pg-author-photo { max-width: 200px; margin: 0 auto; }
}

/* ============================================================================
   IN-GAME MANUAL — header icon + modal
   ============================================================================
   A small "?" / book icon next to the settings cog opens a modal explaining
   the rules of the game (loop, touch, friction, aura tiers, modes, lives).
   Localized via brand_strings (manual_*). The icon is BRAND; the rules
   strings have EN+CA defaults baked into runtime.js. */

#manual-toggle {
  border: none;
  font-size: 1em;
  padding: 0.2em 0.4em;
  color: var(--muted);
  font-family: var(--font-display);
  font-weight: 700;
  letter-spacing: 0.04em;
}
#manual-toggle:hover { color: var(--fg); border: none; }

#manual-modal {
  border: none;
  border-radius: 8px;
  padding: 2rem 2.4rem;
  background: var(--bg);
  color: var(--fg);
  box-shadow: 0 12px 40px rgba(0, 0, 0, 0.18);
  max-width: 640px;
  width: 92%;
  max-height: 85vh;
  overflow-y: auto;
}
#manual-modal::backdrop { background: rgba(0, 0, 0, 0.4); backdrop-filter: blur(4px); }

#manual-modal .manual-title {
  font-family: var(--font-display);
  font-size: 0.95em;
  font-weight: 700;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--muted);
  margin: 0 0 1.4rem 0;
}
#manual-modal .manual-section {
  margin-bottom: 1.2rem;
}
#manual-modal .manual-section-heading {
  font-family: var(--font-display);
  font-size: 0.78em;
  font-weight: 700;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--accent);
  margin-bottom: 0.4rem;
}
#manual-modal .manual-section-body {
  font-family: var(--font-primary);
  font-size: 0.95em;
  line-height: 1.55;
  color: var(--fg);
  white-space: pre-wrap;
}
#manual-modal .manual-close {
  margin-top: 0.6rem;
  display: block;
  margin-left: auto;
}

/* ============================================================================
   PER-MECHANIC POPOVER — first-time hint for touch / friction
   ============================================================================
   A small elegant card that appears the first time a touch (or friction)
   beat opens for interaction. Explains the mechanic in 2-3 lines. Carries
   an X close and a "don't show again" toggle. Dismissals persist in
   localStorage (manual_dismissed_touch / manual_dismissed_friction). */

#mechanic-popover {
  position: fixed;
  z-index: 60;
  bottom: 80px;
  right: 24px;
  max-width: 320px;
  background: var(--bg);
  border: 1px solid var(--rule);
  border-radius: 6px;
  padding: 0.9rem 1.05rem 0.7rem 1.05rem;
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
  font-family: var(--font-primary);
  font-size: 0.92em;
  line-height: 1.5;
  color: var(--fg);
  transform: translateY(8px);
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.25s ease, transform 0.25s ease;
}
#mechanic-popover.is-open {
  opacity: 1;
  transform: translateY(0);
  pointer-events: auto;
}

#mechanic-popover .mp-heading {
  font-family: var(--font-display);
  font-size: 0.72em;
  font-weight: 700;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--accent);
  margin-bottom: 0.35rem;
}
#mechanic-popover .mp-body {
  margin-bottom: 0.7rem;
}
#mechanic-popover .mp-close {
  position: absolute;
  top: 0.4rem;
  right: 0.55rem;
  background: transparent;
  border: none;
  color: var(--muted);
  font-size: 1.1em;
  line-height: 1;
  padding: 0.15em 0.3em;
  cursor: pointer;
}
#mechanic-popover .mp-close:hover { color: var(--fg); }
#mechanic-popover .mp-dismiss {
  display: flex;
  align-items: center;
  gap: 0.5em;
  font-family: var(--font-display);
  font-size: 0.7em;
  letter-spacing: 0.08em;
  color: var(--muted);
  cursor: pointer;
  margin-top: 0.3rem;
}
#mechanic-popover .mp-dismiss input { margin: 0; cursor: pointer; }

body.fusion-mode #mechanic-popover { opacity: 0 !important; pointer-events: none; }
