/* three-stage.css — AAA/three workstream: the dice CEREMONY (the roll card's
 * stage, die, readout and verdict) and the ambient layer. three-stage.js draws
 * the 3D die and the atmosphere; play.jsx RollCard supplies the markup.
 *
 * Built on the design system (.planning/aaa/design/DESIGN.md): scale tokens
 * (--space-*, --text-*, --radius-1/2, --dur-*, --ease-*) and palette tokens.
 * Colour roles: gold = agency ("you" — the kept die's ring), green ONLY for a
 * success verdict, red ONLY for a failure verdict, --verdict-crit for a
 * natural 20; the card itself wears a NEUTRAL frame colour, never a verdict
 * hue and never the mana/success hue.
 *
 * The --rt-* tokens below are this workstream's art direction, one block per
 * palette (same four ids as the theme contract). The verdict inks are chosen
 * per palette to clear 4.5:1 on that palette's card surface (measured in the
 * evidence packet), which a single color-mix() of the shared verdict tokens
 * cannot do in both light and dark palettes. Anything undefined falls back to
 * a derivation from the core palette tokens (three-stage.js readLook).
 */

/* ---- Art direction, per palette ------------------------------------------- */
/* parchment (the bare :root palette): oxblood resin, gilt numerals, a brass
   frame; verdicts in book-ink green / red; ambient = sepia dust in window light. */
:root {
  --rt-die-body: #561a1e;
  --rt-die-ink: #e0b75a;
  --rt-die-rim: #b8862a;
  --rt-die-env: #f2c56e;
  --rt-die-shadow: #3c2614;
  --rt-frame: #9a7b45;
  --rt-ok: #3f6b45;
  --rt-bad: #8b2e2e;
  --rt-part: #7a5a14;
  --rt-crit: #7a560c;
  --rt-crit-fill: #8a6414;
  --rt-crit-fill-ink: #fbf3dc;
  --rt-crit-halo: #a8781a;
  --rt-fumble-fill: #6b1414;
  --rt-fumble-fill-ink: #f6e8d6;
  --rt-fumble-halo: #6b1414;
  --rt-amb-a: #9a7a4c;
  --rt-amb-b: #b0905f;
  --rt-amb-pool: #ffc466;
  --rt-amb-pool-2: #f5b25a;
  --rt-amb-edge: #6a4a26;
}
/* observatory: lapis die, gold numerals, starlight rim. */
:root[data-palette="observatory"] {
  --rt-die-body: #1c2856;
  --rt-die-ink: #e6c97a;
  --rt-die-rim: #9cc0e6;
  --rt-die-env: #9cc0e6;
  --rt-die-shadow: #000000;
  --rt-frame: #9fa6c8;
  --rt-ok: #7fc97f;
  --rt-bad: #f08080;
  --rt-part: #e6b86a;
  --rt-crit: #e6c97a;
  --rt-crit-fill: #d9b45a;
  --rt-crit-fill-ink: #141008;
  --rt-crit-halo: #e6c97a;
  --rt-fumble-fill: #7a1f22;
  --rt-fumble-fill-ink: #f4e6e2;
  --rt-fumble-halo: #d04848;
  --rt-amb-a: #e6c97a;
  --rt-amb-b: #9cc0e6;
  --rt-amb-pool: #d9b45a;
  --rt-amb-pool-2: #7aa2c8;
  --rt-amb-edge: #000000;
}
/* dark-glass (default): umber resin, gold inlay, a muted brass frame; embers
   are candle-gold (AAA i2: the cool motes and blue pools went with the navy). */
:root[data-palette="dark-glass"] {
  --rt-die-body: #2a1c14;  /* AAA i2: warm umber resin (was sapphire) */
  --rt-die-ink: #efc978;
  --rt-die-rim: #e8c06a;
  --rt-die-env: #f0c888;
  --rt-die-shadow: #000000;
  --rt-frame: #b8a67a;
  --rt-ok: #6fd0a0;
  --rt-bad: #ff8275;
  --rt-part: #e8b14e;
  --rt-crit: #ffd98a;
  --rt-crit-fill: #e2b85e;
  --rt-crit-fill-ink: #1b1405;
  --rt-crit-halo: #ffd98a;
  --rt-fumble-fill: #7a1f1c;
  --rt-fumble-fill-ink: #f6e6e2;
  --rt-fumble-halo: #d0453a;
  --rt-amb-a: #f0c878;
  --rt-amb-b: #d9a868;
  --rt-amb-pool: #e8b060;
  --rt-amb-pool-2: #a8703c;
  --rt-amb-edge: #000000;
}
/* arcane: obsidian-violet die, luminous violet inlay, a lavender frame; crits
   stay power-gold; a fumble is deep red, never pastel. */
:root[data-palette="arcane"] {
  --rt-die-body: #1f1238;
  --rt-die-ink: #d6bcff;
  --rt-die-rim: #b78cff;
  --rt-die-env: #b78cff;
  --rt-die-shadow: #000000;
  --rt-frame: #b9aee0;
  --rt-ok: #58e0b0;
  --rt-bad: #f0707e;
  --rt-part: #ffc85e;
  --rt-crit: #ffd75e;
  --rt-crit-fill: #e0b440;
  --rt-crit-fill-ink: #1a1206;
  --rt-crit-halo: #ffd75e;
  --rt-fumble-fill: #7a1b1b;
  --rt-fumble-fill-ink: #f3e6ec;
  --rt-fumble-halo: #c2253a;
  --rt-amb-a: #c9a2ff;
  --rt-amb-b: #5ce0e0;
  --rt-amb-pool: #8a5cff;
  --rt-amb-pool-2: #3cc8ff;
  --rt-amb-edge: #000000;
}

/* ---- Ambient layer --------------------------------------------------------- */
/* Mounted INSIDE the shell's .shell-ambient (DESIGN.md §5: absolute, inset 0,
   z-index 0, inert, behind the header, the prose and the dock). Never above
   text, never a hit target. Fades in once, slowly. */
.rt3-ambient {
  position: absolute;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  opacity: 0;
  transition: opacity 1.8s ease;
}
.rt3-ambient.rt3-live { opacity: 0.55; }  /* AAA i2 — a quiet room, not a starfield */
.rt3-ambient > canvas { display: block; width: 100%; height: 100%; }

/* ---- The dice ceremony: card, stage, readout, verdict ---------------------- */
/* The frame is neutral; the verdict colour arrives only with the verdict, and
   only on the top rule and the verdict label itself. */
html[data-screen="play"] .feed .roll-card {
  --rc-accent: var(--rt-frame);
  --rc-verdict: var(--rt-frame);
  --rc-die: 116px;
  padding: var(--space-5);
}
html[data-screen="play"] .feed .roll-card.success      { --rc-verdict: var(--rt-ok); }
html[data-screen="play"] .feed .roll-card.fail         { --rc-verdict: var(--rt-bad); }
html[data-screen="play"] .feed .roll-card.partial      { --rc-verdict: var(--rt-part); }
html[data-screen="play"] .feed .roll-card.crit-success { --rc-verdict: var(--rt-crit-fill); }
html[data-screen="play"] .feed .roll-card.crit-fail    { --rc-verdict: var(--rt-fumble-fill); }
html[data-screen="play"] .feed .roll-card::before,
html[data-screen="play"] .feed .roll-card.rolling::before { background: var(--rc-verdict); }
html[data-screen="play"] .feed .roll-card.rc-pair { --rc-die: 100px; }

/* Header: the label stays ink; mode tags (advantage…) are neutral words, not
   a third green. */
html[data-screen="play"] .feed .roll-card .roll-card-header { margin-bottom: var(--space-3); }
html[data-screen="play"] .feed .roll-card .roll-card-tag,
html[data-screen="play"] .feed .roll-card .roll-card-tag.adv,
html[data-screen="play"] .feed .roll-card .roll-card-tag.dis,
html[data-screen="play"] .feed .roll-card .roll-card-tag.rerolled {
  font-variant-caps: normal;
  letter-spacing: 0;
  font-weight: 500;
  color: var(--ink-2);
  border-color: var(--border-strong);
}
/* "Rolling" is a transient state word: it sits OUT of the header's flow, so
   its arrival and departure can never re-wrap the header and move the stage
   (which would shift the card mid-ceremony and misplace the die). */
html[data-screen="play"] .feed .roll-card .roll-card-header { position: relative; }
html[data-screen="play"] .feed .roll-card .roll-card-tag.rolling-tag {
  position: absolute;
  right: 0;
  top: 50%;
  transform: translateY(-50%);
  margin: 0;
  color: var(--ink-faded);
  border-color: transparent;
  font-style: italic;
}

/* Stage: die | readout. The die slot is a fixed box from mount, so the card's
   height never changes mid-ceremony (the ScrollPolicy snap stays truthful). */
html[data-screen="play"] .feed .roll-card .roll-card-stage {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  column-gap: var(--space-5);
  align-items: center;
  margin: 0;
}
html[data-screen="play"] .feed .roll-card .roll-card-dice {
  position: relative;
  display: flex;
  flex-direction: row;
  flex-wrap: nowrap;
  align-items: flex-start;
  justify-content: center;
  gap: var(--space-4);
  margin: 0;
  min-height: var(--rc-die);
}
html[data-screen="play"] .feed .roll-card .roll-card-dice[data-kept="right"] .d20-face.primary { order: 2; }
/* Each die is a square box (plus a caption line for a pair). The hexagon is
   the DOM fallback face; the 3D die is drawn over the same box. */
html[data-screen="play"] .feed .roll-card .d20-face,
html[data-screen="play"] .feed .roll-card .d20-face.secondary {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: flex-start;
  width: var(--rc-die);
  min-width: var(--rc-die);
  height: auto;
  min-height: var(--rc-die);
  padding: 0;
  opacity: 1;
}
html[data-screen="play"] .feed .roll-card .d20-face::before {
  inset: 0 0 auto 0;
  height: var(--rc-die);
}
html[data-screen="play"] .feed .roll-card .d20-face .d20-num,
html[data-screen="play"] .feed .roll-card .d20-face.secondary .d20-num {
  display: flex;
  align-items: center;
  justify-content: center;
  height: var(--rc-die);
  font-size: calc(var(--rc-die) * 0.34);
}
html[data-screen="play"] .feed .roll-card .d20-face .d20-tag {
  margin-top: var(--space-2);
  font-size: var(--text-sm);
  font-variant-caps: normal;
  letter-spacing: 0;
  line-height: 1.2;
  color: var(--ink-faded);
}
html[data-screen="play"] .feed .roll-card .d20-face.primary .d20-tag { color: var(--ink); font-weight: 600; }
/* After both dice rest: the kept die takes a gold ring (agency: this one is
   yours), the discarded one steps back. The DOM fallback face shrinks too;
   with the 3D die the renderer does the shrinking and the ring is this. */
html[data-screen="play"] .feed .roll-card .roll-card-dice .d20-face.primary::after {
  content: '';
  position: absolute;
  left: 50%;
  top: calc(var(--rc-die) / 2);
  width: calc(var(--rc-die) + 12px);
  height: calc(var(--rc-die) + 12px);
  border: 2px solid var(--gold);
  border-radius: var(--radius-round);
  transform: translate(-50%, -50%) scale(0.92);
  opacity: 0;
  pointer-events: none;
}
html[data-screen="play"] .feed .roll-card .roll-card-dice.rc-picked .d20-face.primary::after {
  opacity: 0.85;
  transform: translate(-50%, -50%) scale(1);
}
html[data-screen="play"] .feed .roll-card .roll-card-dice.rc-picked:not([data-rt3]) .d20-face.secondary::before,
html[data-screen="play"] .feed .roll-card .roll-card-dice.rc-picked:not([data-rt3]) .d20-face.secondary .d20-num {
  opacity: 0.55;
  transform: scale(0.72);
}

/* Readout: ONE serif sentence, lining tabular figures, no monospace. */
html[data-screen="play"] .feed .roll-card .roll-card-flow {
  display: flex;
  flex-direction: row;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-5);
  min-width: 0;
}
html[data-screen="play"] .feed .roll-card.rc-pair .roll-card-flow {
  flex-direction: column;
  align-items: flex-start;
  justify-content: center;
  gap: var(--space-3);
}
html[data-screen="play"] .feed .roll-card .rc-sentence {
  margin: 0;
  font-family: var(--font-serif);
  font-size: 22px;
  line-height: 1.35;
  font-variant-numeric: lining-nums tabular-nums;
  color: var(--ink);
  white-space: nowrap;
}
html[data-screen="play"] .feed .roll-card .rc-roll {
  font-weight: 700;
  color: var(--ink);
}
html[data-screen="play"] .feed .roll-card .rc-roll:not(.rc-known) {
  font-weight: 500;
  font-style: italic;
  color: var(--ink-faded);
}
html[data-screen="play"] .feed .roll-card .rc-op,
html[data-screen="play"] .feed .roll-card .rc-vs { color: var(--ink-faded); }
html[data-screen="play"] .feed .roll-card .rc-vs { font-style: italic; }
html[data-screen="play"] .feed .roll-card .rc-num { font-weight: 600; color: var(--ink); }
html[data-screen="play"] .feed .roll-card .rc-mod-label { color: var(--ink-2); }
html[data-screen="play"] .feed .roll-card .rc-dc-label { color: var(--ink-2); letter-spacing: 0.02em; }
html[data-screen="play"] .feed .roll-card .rc-total strong {
  font-size: 1.32em;
  font-weight: 700;
  line-height: 1;
}

/* Verdict: a label with hairline rules above and below, square-ended, in its
   semantic ink — not a filled pill, never a button silhouette. A natural 20 or
   a natural 1 earns a filled, notched ribbon of its own. */
html[data-screen="play"] .feed .roll-card .roll-card-verdict,
html[data-screen="play"] .feed .roll-card .roll-card-verdict.crit-success,
html[data-screen="play"] .feed .roll-card .roll-card-verdict.crit-fail {
  margin: 0;
  padding: var(--space-1) var(--space-1);
  border: 0;
  border-top: 1px solid currentColor;
  border-bottom: 1px solid currentColor;
  border-radius: 0;
  background: none;
  font-family: var(--font-serif);
  font-size: var(--text-lg);
  font-weight: 600;
  line-height: 1.25;
  font-variant-caps: all-small-caps;
  letter-spacing: 0.14em;
  text-transform: none;
  white-space: nowrap;
  color: var(--ink-2);
  flex-shrink: 0;
}
html[data-screen="play"] .feed .roll-card .roll-card-verdict.success { color: var(--rt-ok); }
html[data-screen="play"] .feed .roll-card .roll-card-verdict.fail    { color: var(--rt-bad); }
html[data-screen="play"] .feed .roll-card .roll-card-verdict.partial { color: var(--rt-part); }
html[data-screen="play"] .feed .roll-card .roll-card-verdict.crit-success,
html[data-screen="play"] .feed .roll-card .roll-card-verdict.crit-fail {
  padding: var(--space-1) var(--space-5);
  border: 0;
  clip-path: polygon(0 0, 100% 0, calc(100% - 10px) 50%, 100% 100%, 0 100%, 10px 50%);
}
html[data-screen="play"] .feed .roll-card .roll-card-verdict.crit-success {
  background: var(--rt-crit-fill);
  color: var(--rt-crit-fill-ink);
}
html[data-screen="play"] .feed .roll-card .roll-card-verdict.crit-fail {
  background: var(--rt-fumble-fill);
  color: var(--rt-fumble-fill-ink);
}

/* Phone: the sentence may wrap (two lines reserved from mount so the card
   height never changes when "= 16" arrives); the verdict sits under it. A
   pair of dice sits in its own row above the readout. */
@media (max-width: 600px) {
  html[data-screen="play"] .feed .roll-card { --rc-die: 120px; padding: var(--space-4); }
  html[data-screen="play"] .feed .roll-card.rc-pair { --rc-die: 104px; }
  html[data-screen="play"] .feed .roll-card .roll-card-stage { column-gap: var(--space-4); }
  html[data-screen="play"] .feed .roll-card .roll-card-flow {
    flex-direction: column;
    align-items: flex-start;
    justify-content: center;
    gap: var(--space-3);
  }
  html[data-screen="play"] .feed .roll-card .rc-sentence {
    white-space: normal;
    font-size: 21px;
    min-height: calc(2 * 1.35em);
  }
  html[data-screen="play"] .feed .roll-card.rc-pair .roll-card-stage {
    grid-template-columns: minmax(0, 1fr);
    row-gap: var(--space-4);
  }
  html[data-screen="play"] .feed .roll-card.rc-pair .rc-sentence { min-height: 0; }
  /* A narrow readout column: the verdict tightens its tracking, and a
     crit/fumble ribbon keeps its notched ends inside the card. */
  html[data-screen="play"] .feed .roll-card .roll-card-verdict,
  html[data-screen="play"] .feed .roll-card .roll-card-verdict.crit-success,
  html[data-screen="play"] .feed .roll-card .roll-card-verdict.crit-fail {
    letter-spacing: 0.08em;
    max-width: 100%;
  }
  html[data-screen="play"] .feed .roll-card .roll-card-verdict.crit-success,
  html[data-screen="play"] .feed .roll-card .roll-card-verdict.crit-fail {
    padding: var(--space-1) var(--space-4);
    font-size: var(--text-md);
    letter-spacing: 0.05em;
  }
}

/* Motion (all of it silent under reduced motion: the rules only exist under
   no-preference). The card is fully opaque from its first frame and lifts
   8px into place; the spoken total and the verdict arrive with a short
   press, not a 3x stamp. */
@media (prefers-reduced-motion: no-preference) {
  html[data-screen="play"] .feed .roll-card.fade-in {
    animation: rt3CardIn 140ms var(--ease-out) both;
  }
  html[data-screen="play"] .feed .roll-card .rc-roll.rc-known,
  html[data-screen="play"] .feed .roll-card .rc-total {
    animation: rt3Speak 220ms var(--ease-out) both;
  }
  html[data-screen="play"] .feed .roll-card .roll-card-verdict.cx-stamp {
    animation: rt3Verdict 320ms var(--ease-emph) both;
  }
  html[data-screen="play"] .feed .roll-card .roll-card-dice .d20-face.primary::after {
    transition: opacity var(--dur-slow) var(--ease-out), transform var(--dur-slow) var(--ease-emph);
  }
  html[data-screen="play"] .feed .roll-card .roll-card-dice:not([data-rt3]) .d20-face.secondary::before,
  html[data-screen="play"] .feed .roll-card .roll-card-dice:not([data-rt3]) .d20-face.secondary .d20-num {
    transition: opacity var(--dur-slow) var(--ease-out), transform var(--dur-slow) var(--ease-out);
  }
}
@keyframes rt3CardIn {
  from { transform: translateY(8px); }
  to   { transform: none; }
}
@keyframes rt3Speak {
  from { opacity: 0; transform: translateY(3px); }
  to   { opacity: 1; transform: none; }
}
@keyframes rt3Verdict {
  from { opacity: 0; transform: scale(1.18); letter-spacing: 0.3em; }
  to   { opacity: 1; transform: scale(1); letter-spacing: 0.14em; }
}

/* ---- The 3D die over the card ---------------------------------------------- */
/* three-stage.js appends one clipped layer to the card and draws each die in
   its own canvas inside it, over that die's box. The DOM face keeps its box,
   its number (the roll's accessible text) and its aria-label; only its paint
   yields — opacity, not visibility, so the text stays in the a11y tree. */
html[data-screen="play"] .roll-card .rt3-card-layer {
  position: absolute;
  inset: 0;
  overflow: hidden;
  border-radius: inherit;
  pointer-events: none;
  z-index: 1;
}
html[data-screen="play"] .roll-card .rt3-die {
  position: absolute;
  pointer-events: none;
  opacity: 0;
}
html[data-screen="play"] .roll-card .rt3-die.rt3-live { opacity: 1; }
@media (prefers-reduced-motion: no-preference) {
  html[data-screen="play"] .feed .roll-card .roll-card-dice[data-rt3] .d20-face::before,
  html[data-screen="play"] .feed .roll-card .roll-card-dice[data-rt3] .d20-face .d20-num {
    opacity: 0;
    animation: none;
  }
  html[data-screen="play"] .feed .roll-card .roll-card-dice[data-rt3] .d20-face.tumbling { animation: none; }
  /* A card that mounted before Three.js arrived upgrades with a short
     crossfade instead of a pop; a live ceremony swaps before first paint. */
  html[data-screen="play"] .roll-card:has(.roll-card-dice[data-rt3="fade"]) .rt3-die { transition: opacity var(--dur-base) ease; }
}
@media (prefers-reduced-motion: reduce) {
  .rt3-ambient,
  html[data-screen="play"] .roll-card .rt3-card-layer { display: none; }
}
