/* ── Magic 8 Ball ─────────────────────────────────────────────────────────── */

:root {
  /* ── Type tiers ─────────────────────────────────────────────────────────────
   * Every text element sits in exactly one tier, and adjacent tiers are at
   * least 1.3x apart, so nothing can land in between: text of a similar size is
   * identical in treatment, and text of a different size is unmistakably
   * different. See the "Type tiers" block below for the assignments. */
  --tier-1: clamp(20px, 3.5vmin, 34px);
  --tier-2: max(14px, calc(var(--tier-1) / 1.7));
  --tier-3: max(10.5px, calc(var(--tier-2) / 1.3));
  --tier-3-tracking: 0.12em;

  /* ── Spacing scale ──────────────────────────────────────────────────────────
   * The vertical rhythm between masthead, ball, readout, breakdown, form and
   * footer uses only these four steps. */
  --space-1: 0.35rem;
  --space-2: 0.7rem;
  --space-3: 1.2rem;
  --space-4: 2rem;

  /* ── Ink levels: exactly three text colours, used by role ─────────────────── */
  --ink: #eceaf2;        /* primary: wordmark, answer, field text */
  --muted: #8f8fa6;      /* secondary: values, band, percentages, summary */
  --muted-dim: #6c6c80;  /* tertiary: labels, hint, counter, note, footer */
  /* The light button surface needs the inverse ink; it is not a fourth level
     on the dark backdrop, it is the same primary ink inverted. */
  --ink-inverse: #0b0b11;

  /* State colours: the counter and the inline cap message only. */
  --state-warn: #d9a441;
  --state-danger: #d9564e;

  /* Surfaces and rules (never text). */
  --surface: rgba(255, 255, 255, 0.045);
  --surface-raised: rgba(255, 255, 255, 0.07);
  --hairline: rgba(255, 255, 255, 0.11);

  /* One label column and one value column, shared by the ODDS/CONFIDENCE rows
     and the breakdown fact rows so every label and number lines up. */
  --label-col: 12rem;
  --value-col: 8rem;

  /* The ball is the anchor, but on a short viewport the vh guard wins over
     clipping the stacked form. */
  --ball-size: min(clamp(240px, 38vmin, 380px), 36vh);
  --accent: #4ea1ff;
  --serif: Georgia, "Times New Roman", Times, serif;
  --sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  --mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  --reveal: 450ms;
}

body[data-polarity="affirmative"] { --accent: #4ea1ff; }
body[data-polarity="non_committal"] { --accent: #d9a441; }
body[data-polarity="negative"] { --accent: #d9564e; }

* { box-sizing: border-box; }

/* html owns the viewport height and body floors at it, but body must be free
   to grow: a fixed height here makes .page a definite-height flex container,
   and its children then shrink into each other instead of the document
   scrolling. */
html { height: 100%; }
body { min-height: 100%; }

body {
  margin: 0;
  font-family: var(--sans);
  color: var(--ink);
  /* Space backdrop. Back to front: a deep blue-black vertical gradient that
     reads as depth, then three very large, heavily feathered nebula blooms in
     indigo/violet (peak alpha under 0.12 — atmosphere, not scenery), then a
     soft pool of light under the ball so it keeps its own stage. The stars
     paint on top of all of this from #starfield. */
  background:
    radial-gradient(52% 44% at 17% 18%, rgba(86, 68, 192, 0.12), rgba(86, 68, 192, 0) 68%),
    radial-gradient(46% 40% at 85% 24%, rgba(128, 74, 210, 0.10), rgba(128, 74, 210, 0) 70%),
    radial-gradient(66% 58% at 63% 90%, rgba(56, 84, 208, 0.08), rgba(56, 84, 208, 0) 72%),
    radial-gradient(72% 48% at 50% 44%, rgba(158, 184, 255, 0.055), rgba(158, 184, 255, 0) 70%),
    linear-gradient(180deg, #05060c 0%, #080a15 56%, #0b0d1a 100%);
  display: flex;
  justify-content: center;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

/* Star field: sized to the viewport, painted by paintStars() in app.js. */
#starfield {
  position: fixed;
  inset: 0;
  display: block;
  width: 100%;
  height: 100%;
  z-index: 0;
  pointer-events: none;
}

/* Vignette: darkens the corners of the star layer only. It sits above the
   canvas but below .page, so the ball and the readout stay in their pool. */
body::after {
  content: "";
  position: fixed;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  background: radial-gradient(118% 86% at 50% 44%,
    rgba(0, 0, 0, 0) 0%,
    rgba(0, 0, 0, 0.10) 44%,
    rgba(0, 0, 0, 0.44) 78%,
    rgba(0, 0, 0, 0.64) 100%);
}

.page {
  position: relative;
  z-index: 2;
  width: min(760px, 100%);
  min-height: 100dvh;
  padding: clamp(12px, 1.6vh, 26px) clamp(14px, 3vw, 30px);
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
}

/* Desktop type scale: the layout is rem-based, so one root step grows the
   whole readout, the field and the meters together with the ball. */
@media (min-width: 1200px) { :root { font-size: 18.5px; } }
@media (min-width: 1700px) { :root { font-size: 19.5px; } }

/* Taller viewports spend their slack on the rhythm, from the same scale, so
   the content spans the screen instead of clustering at the top. */
@media (min-height: 1250px) { .page { gap: var(--space-3); } }

/* Short viewports give up ball size and inner rhythm before they give up type
   or clip the stacked form. */
@media (max-height: 950px) {
  :root { --ball-size: min(clamp(240px, 38vmin, 380px), 33vh); }
  .readout { gap: var(--space-1); }
}

/* ── Type tiers ───────────────────────────────────────────────────────────────
 * Tier 1  wordmark + serif answer      — largest, primary ink
 * Tier 2  field text + breakdown title — mid
 * Tier 3  everything meta              — uppercase, tracked, dimmed
 * ------------------------------------------------------------------------- */

.wordmark,
.answer {
  font-size: var(--tier-1);
}

.question,
.breakdown > summary {
  font-size: var(--tier-2);
}

.hint,
.chip,
.band,
.meta-label,
.pct,
.fact dt,
.fact dd,
.fact-hint,
.note,
.override,
.failure,
.counter,
.field-error,
.shake,
.footer {
  font-size: var(--tier-3);
  letter-spacing: var(--tier-3-tracking);
  text-transform: uppercase;
}

/* ── Masthead ─────────────────────────────────────────────────────────────── */

.masthead { text-align: center; }

.wordmark {
  margin: 0;
  font-weight: 700;
  letter-spacing: 0.42em;
  text-indent: 0.42em;
  color: var(--ink);
  text-transform: uppercase;
}

.hint {
  margin: var(--space-2) 0 0;
  color: var(--muted-dim);
}

/* ── The ball ─────────────────────────────────────────────────────────────── */

/* The page gap already separates the stage from the masthead and the readout,
   so the stage carries no padding of its own. */
.stage {
  display: flex;
  justify-content: center;
  align-items: center;
}

.ball-stage {
  position: relative;
  width: var(--ball-size);
  height: var(--ball-size);
}

.ball-shake {
  width: 100%;
  height: 100%;
  transform-origin: 50% 58%;
  will-change: transform;
}

.ball {
  container-type: size;
  position: relative;
  width: 100%;
  height: 100%;
  border-radius: 50%;
  background:
    radial-gradient(circle at 30% 22%,
      rgba(255, 255, 255, 0.44) 0%,
      rgba(255, 255, 255, 0.16) 21%,
      rgba(255, 255, 255, 0.05) 34%,
      rgba(255, 255, 255, 0) 52%),
    radial-gradient(circle at 79% 86%,
      rgba(160, 186, 255, 0.30) 0%,
      rgba(160, 186, 255, 0.10) 20%,
      rgba(160, 186, 255, 0) 42%),
    radial-gradient(circle at 62% 74%,
      rgba(255, 255, 255, 0.05) 0%,
      rgba(255, 255, 255, 0) 30%),
    radial-gradient(circle at 50% 42%,
      #26262e 0%,
      #17171d 32%,
      #0a0a0c 62%,
      #050506 88%,
      #0a0a0c 100%);
  box-shadow:
    inset 0 -14px 26px rgba(0, 0, 0, 0.65),
    inset -9px -12px 22px -8px rgba(158, 186, 255, 0.30),
    inset 0 10px 22px rgba(255, 255, 255, 0.05),
    0 26px 44px -20px rgba(0, 0, 0, 0.9),
    0 10px 18px rgba(0, 0, 0, 0.55);
  transform-origin: 50% 58%;
  will-change: transform, filter;
}

/* Contact glow under the ball. A single-stop falloff and a wide blur: two
   gradient stops with different slopes, plus a tight blur, read as a visible
   ellipse edge crossing the ball's lower rim. */
.reflection {
  position: absolute;
  left: 50%;
  top: calc(100% + 1%);
  width: 70%;
  height: 14%;
  transform: translateX(-50%);
  border-radius: 50%;
  background: radial-gradient(ellipse at center,
    rgba(150, 178, 255, 0.16) 0%,
    rgba(150, 178, 255, 0) 62%);
  filter: blur(18px);
  pointer-events: none;
}

/* Polarity accent: a bloom on the casing only. Its inner edge starts at the
   window's radius (62% / 2 = 31%), so no accent colour can reach the die face. */
.ball::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: 50%;
  z-index: -1;
  pointer-events: none;
  opacity: 0;
  transition: opacity var(--reveal) ease;
  background: radial-gradient(circle at 50% 50%,
    rgba(0, 0, 0, 0) 0 31%,
    color-mix(in srgb, var(--accent) 62%, transparent) 34%,
    color-mix(in srgb, var(--accent) 20%, transparent) 44%,
    rgba(0, 0, 0, 0) 60%);
}

.ball[data-state="revealed"]::after,
.ball[data-state="error"]::after { opacity: 1; }

/* window */

.window {
  position: absolute;
  inset: 0;
  margin: auto;
  width: 62%;
  height: 62%;
  border-radius: 50%;
  overflow: hidden;
  background: radial-gradient(circle at 50% 40%, #ffffff 0%, #f4f5fb 62%, #d6d9ec 100%);
  box-shadow:
    inset 0 3px 7px rgba(0, 0, 0, 0.30),
    inset 0 -2px 3px rgba(255, 255, 255, 0.85),
    inset 0 0 0 1px rgba(0, 0, 0, 0.05),
    0 0 0 1px rgba(214, 221, 238, 0.72),
    0 0 0 3px rgba(7, 7, 11, 0.85),
    0 4px 10px rgba(0, 0, 0, 0.45);
}

.face {
  position: absolute;
  inset: 0;
  transition: opacity var(--reveal) ease, transform var(--reveal) ease;
}

.face-8 {
  display: grid;
  place-items: center;
  align-content: center;
  /* A single auto row overflows the content box, so the line box is centred by
     align-content; this nudge puts the ink (not the line box) on the optical
     centre of the window. Values verified against rendered pixels. */
  padding-top: 1cqmin;
  font-family: var(--serif);
  font-weight: 700;
  font-size: 71.5cqmin;
  line-height: 1;
  color: #14141a;
  text-shadow:
    0 1px 0 rgba(255, 255, 255, 0.7),
    0 -2px 4px rgba(0, 0, 0, 0.35);
  opacity: 1;
}

.face-answer {
  opacity: 0;
  transform: scale(0.96);
  background: radial-gradient(circle at 50% 50%, #121a3c 0%, #0a0c1a 58%, #05060c 100%);
}

.ball[data-state="revealed"] .face-8,
.ball[data-state="error"] .face-8 { opacity: 0; transform: scale(0.96); }

.ball[data-state="revealed"] .face-answer,
.ball[data-state="error"] .face-answer { opacity: 1; transform: scale(1); }

/* the answer face: a point-down triangle inscribed in the window circle —
   flat top edge at 25% of the circle height, base 0.866 W, apex at the bottom */

.tri-glow {
  position: absolute;
  left: 6.7%;
  top: 25%;
  width: 86.6%;
  height: 75%;
  /* No accent filter here: a drop-shadow of this shape paints a second
     horizontal edge just above the base stroke and bleeds accent colour onto
     the die face. The accent lives on the casing (see .ball::after) instead. */
}

.tri {
  display: block;
  width: 100%;
  height: 100%;
  overflow: visible;
}

.tri-base { fill: #08090f; }
.tri-light { fill: url(#orbTriGlow); }

.tri-edge {
  fill: none;
  stroke: #1e3fd8;
  stroke-width: clamp(1.5px, 0.65cqmin, 2.6px);
  vector-effect: non-scaling-stroke;
  stroke-linejoin: miter;
}

.tri-slot {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  /* lifts the block's optical centre to 42% of the window height */
  padding-bottom: 16%;
  pointer-events: none;
}

.tri-text {
  margin: 0 auto;
  max-width: 60%;
  text-align: center;
  font-family: var(--serif);
  font-weight: 700;
  font-size: clamp(10px, 5.9cqmin, 23px); /* replaced per answer by fitFaceText() */
  line-height: 1.15;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  text-wrap: balance;
  overflow-wrap: normal;
  color: #ffffff;
  text-shadow: 0 1px 2px rgba(0, 0, 0, 0.5);
}

.tri-text[hidden] { display: none; }

.tri-text.is-failure {
  font-style: italic;
  font-weight: 400;
  letter-spacing: 0.02em;
  text-transform: none;
  overflow-wrap: normal;
  color: color-mix(in srgb, var(--accent) 62%, #dfe3f2);
}

/* ── Motion ───────────────────────────────────────────────────────────────── */

@keyframes shake {
  0%   { transform: translate3d(0, 0, 0) rotate(0deg); }
  6%   { transform: translate3d(0, -11px, 0) rotate(-9deg); }
  14%  { transform: translate3d(0, 7px, 0) rotate(11deg); }
  22%  { transform: translate3d(0, -9px, 0) rotate(-12deg); }
  30%  { transform: translate3d(0, 6px, 0) rotate(9deg); }
  38%  { transform: translate3d(0, -9px, 0) rotate(-10deg); }
  46%  { transform: translate3d(0, 4px, 0) rotate(8deg); }
  54%  { transform: translate3d(0, -8px, 0) rotate(-7deg); }
  62%  { transform: translate3d(0, 3px, 0) rotate(6deg); }
  70%  { transform: translate3d(0, -5px, 0) rotate(-4.5deg); }
  78%  { transform: translate3d(0, 2px, 0) rotate(3deg); }
  86%  { transform: translate3d(0, -2.5px, 0) rotate(-2deg); }
  94%  { transform: translate3d(0, 1px, 0) rotate(1deg); }
  100% { transform: translate3d(0, 0, 0) rotate(0deg); }
}

@keyframes ballPulse {
  0%   { transform: scale(1); filter: blur(0); }
  8%   { transform: scale(1.03); filter: blur(0.6px); }
  30%  { transform: scale(1.015); filter: blur(0.3px); }
  100% { transform: scale(1); filter: blur(0); }
}

@keyframes faceJitter {
  0%   { transform: translate3d(0, 0, 0) rotate(0deg) scale(1); }
  8%   { transform: translate3d(0, -4%, 0) rotate(-13deg) scale(1.03); }
  18%  { transform: translate3d(0, 3%, 0) rotate(14deg) scale(1.03); }
  28%  { transform: translate3d(0, -3%, 0) rotate(-11deg) scale(1.02); }
  40%  { transform: translate3d(0, 3%, 0) rotate(11deg) scale(1.02); }
  52%  { transform: translate3d(0, -3%, 0) rotate(-8deg) scale(1.015); }
  64%  { transform: translate3d(0, 2%, 0) rotate(7deg) scale(1.01); }
  76%  { transform: translate3d(0, -2%, 0) rotate(-4deg) scale(1.005); }
  88%  { transform: translate3d(0, 1%, 0) rotate(2deg) scale(1.002); }
  100% { transform: translate3d(0, 0, 0) rotate(0deg) scale(1); }
}

@keyframes rock {
  0%   { transform: rotate(-2.6deg) translateY(-1px); }
  100% { transform: rotate(2.6deg) translateY(1px); }
}

.ball-shake.is-shaking { animation: shake 1.4s cubic-bezier(0.4, 0.05, 0.25, 1) forwards; }
.ball-shake.is-rocking { animation: rock 1.15s ease-in-out infinite alternate; }
.ball.is-pulsing { animation: ballPulse 1.4s cubic-bezier(0.4, 0.05, 0.25, 1) forwards; }
.face-8.is-jittering { animation: faceJitter 1.4s cubic-bezier(0.4, 0.05, 0.25, 1) forwards; }

/* ── Readout ──────────────────────────────────────────────────────────────── */

.readout {
  width: 100%;
  max-width: 540px;
  min-height: 6.4rem;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-2);
  text-align: center;
}

.readout .answer,
.readout .meta,
.readout .breakdown,
.readout .failure {
  transition: opacity var(--reveal) ease, transform var(--reveal) ease;
}

.readout.is-pending .answer,
.readout.is-pending .meta,
.readout.is-pending .breakdown,
.readout.is-pending .failure {
  opacity: 0;
  transform: translateY(8px);
}

.answer {
  margin: 0;
  font-family: var(--serif);
  line-height: 1.22;
  letter-spacing: 0.01em;
  color: var(--ink);
  text-wrap: balance;
}

.meta {
  width: 100%;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-2);
}

.meta[hidden] { display: none; }

.chips {
  display: flex;
  align-items: baseline;
  justify-content: center;
  gap: var(--space-2);
  flex-wrap: wrap;
}

.chip {
  padding: 0.25em 0.7em;
  border-radius: 999px;
  color: var(--accent);
  background: color-mix(in srgb, var(--accent) 12%, transparent);
  border: 1px solid color-mix(in srgb, var(--accent) 38%, transparent);
}

.band {
  color: var(--muted);
}

.metric-row {
  width: 100%;
  display: grid;
  grid-template-columns: var(--label-col) minmax(0, 1fr) var(--value-col);
  align-items: center;
  gap: var(--space-2);
}

/* Column 1 is the shared label column and column 3 the shared value column:
   the ODDS/CONFIDENCE rows and the fact rows below line up on both. */
.metric-row .meta-label { text-align: left; }

.meta-label {
  color: var(--muted-dim);
  white-space: nowrap;
}

/* Odds: a diverging meter centred on a coin flip. Fill grows right of centre
   when the odds favour yes, left when they favour no. */
.diverge {
  position: relative;
  height: 8px;
  border-radius: 999px;
  background: var(--surface-raised);
  box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.5);
  overflow: hidden;
}

.diverge-centre {
  position: absolute;
  left: 50%;
  top: 0;
  bottom: 0;
  width: 1px;
  margin-left: -0.5px;
  background: rgba(255, 255, 255, 0.3);
}

.diverge-fill {
  position: absolute;
  top: 0;
  bottom: 0;
  left: 50%;
  width: 0;
  background: linear-gradient(90deg,
    color-mix(in srgb, var(--accent) 45%, transparent),
    var(--accent));
  transition: width 600ms cubic-bezier(0.2, 0.7, 0.2, 1);
}

.diverge.is-right .diverge-fill {
  left: 50%;
  right: auto;
  border-radius: 0 999px 999px 0;
}

.diverge.is-left .diverge-fill {
  left: auto;
  right: 50%;
  border-radius: 999px 0 0 999px;
  background: linear-gradient(270deg,
    color-mix(in srgb, var(--accent) 45%, transparent),
    var(--accent));
}

.meter {
  height: 8px;
  border-radius: 999px;
  background: var(--surface-raised);
  box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.5);
  overflow: hidden;
}

.meter-fill {
  display: block;
  width: 0;
  height: 100%;
  border-radius: 999px;
  background: linear-gradient(90deg,
    color-mix(in srgb, var(--accent) 45%, transparent),
    var(--accent));
  box-shadow: 0 0 10px color-mix(in srgb, var(--accent) 55%, transparent);
  transition: width 600ms cubic-bezier(0.2, 0.7, 0.2, 1);
}

.pct {
  font-family: var(--mono);
  color: var(--muted);
  min-width: 3.2em;
  text-align: right;
  font-variant-numeric: tabular-nums;
}

.pct.is-odds { min-width: 4.8em; }

.failure {
  margin: 0;
  min-height: 1.2em;
  line-height: 1.45;
  color: var(--muted);
}

.failure[hidden] { display: none; }

/* breakdown */

.breakdown {
  width: 100%;
  border-top: 1px solid var(--hairline);
  padding-top: var(--space-2);
  text-align: left;
}

.breakdown[hidden] { display: none; }

.breakdown > summary {
  cursor: pointer;
  list-style: none;
  text-align: center;
  color: var(--muted);
  padding-block: var(--space-1);
  user-select: none;
}

.breakdown > summary::-webkit-details-marker { display: none; }

.breakdown > summary::before {
  content: "▸ ";
  color: var(--muted-dim);
}

.breakdown[open] > summary::before { content: "▾ "; }

.breakdown > summary:hover { color: var(--ink); }

.facts {
  margin: var(--space-1) 0;
  display: grid;
  grid-template-columns: 1fr;
  gap: 1px;
}

/* The same three columns as .metric-row, so a fact value sits under a
   percentage and a fact label under a meter label. */
.fact {
  display: grid;
  grid-template-columns: var(--label-col) minmax(0, 1fr) var(--value-col);
  align-items: baseline;
  gap: var(--space-2);
  line-height: 1.5;
}

.fact dt {
  color: var(--muted-dim);
}

.fact dd {
  grid-column: 3;
  margin: 0;
  font-family: var(--mono);
  color: var(--muted);
  font-variant-numeric: tabular-nums;
  text-align: right;
}

.note,
.override {
  margin: var(--space-2) 0 0;
  line-height: 1.5;
  color: var(--muted-dim);
}

.override {
  padding-left: var(--space-2);
  box-shadow: inset 2px 0 0 var(--hairline);
}

.override[hidden] { display: none; }

/* ── Ask form ─────────────────────────────────────────────────────────────── */

.ask {
  width: 100%;
  max-width: 540px;
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

.question {
  width: 100%;
  font-family: var(--sans);
  font-size: var(--tier-2);
  line-height: 1.45;
  color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--hairline);
  border-radius: 10px;
  padding: var(--space-2) var(--space-3);
  outline: none;
  /* The field is four rows by default (rows="4"); this is the same four rows
     as a floor, so a manual drag cannot shrink it below the default. */
  min-height: calc(var(--tier-2) * 5.8 + var(--space-2) * 2 + 2px);
  /* A huge paste scrolls inside the field rather than pushing the page off
     screen. JS auto-grows to fit content up to this ceiling. */
  max-height: 40vh;
  overflow-y: auto;
  resize: vertical;
  transition: border-color 160ms ease, background-color 160ms ease, box-shadow 160ms ease;
}

/* The counter row sits under the field, right-aligned; the inline cap message
   takes the space to its left when it appears. */
.field-meta {
  display: flex;
  align-items: baseline;
  justify-content: flex-end;
  gap: var(--space-2);
  margin: 0;
}

.counter {
  font-family: var(--mono);
  font-variant-numeric: tabular-nums;
  color: var(--muted-dim);
  white-space: nowrap;
  transition: color 160ms ease;
}

.counter.is-warn { color: var(--state-warn); }
.counter.is-over { color: var(--state-danger); }

.field-error {
  flex: 1 1 auto;
  min-width: 0;
  text-align: left;
  line-height: 1.45;
  color: var(--state-danger);
}

.field-error[hidden] { display: none; }

.question::placeholder { color: var(--muted-dim); }

.question:focus {
  border-color: color-mix(in srgb, var(--accent) 60%, transparent);
  background: var(--surface-raised);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 18%, transparent);
}

.question:disabled {
  color: var(--muted-dim);
  background: var(--surface-raised);
  cursor: not-allowed;
}

.shake {
  width: 100%;
  height: 3rem;
  font-family: var(--sans);
  font-weight: 600;
  color: var(--ink-inverse);
  background: linear-gradient(180deg, #ffffff, #cfd4ea);
  border: 0;
  border-radius: 10px;
  padding: 0 var(--space-3);
  cursor: pointer;
  transition: filter 140ms ease, transform 140ms ease, background 140ms ease, color 140ms ease;
}

.shake:hover:not(:disabled) { filter: brightness(1.06); }
.shake:active:not(:disabled) { transform: translateY(1px); }

.shake:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

.shake:disabled {
  background: linear-gradient(180deg, #565b6e, #43475a);
  color: var(--muted-dim);
  cursor: not-allowed;
}

.footer {
  color: var(--muted-dim);
  text-align: center;
}

.footer a {
  color: inherit;
  text-decoration: none;
  border-bottom: 1px solid var(--hairline);
  transition: color 140ms ease, border-color 140ms ease;
}

.footer a:hover {
  color: var(--ink);
  border-bottom-color: var(--accent);
}

/* Keyboard focus is visible on every interactive element. */
.shake:focus-visible,
.footer a:focus-visible,
.breakdown > summary:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: 3px;
}

/* ── Utilities & preferences ──────────────────────────────────────────────── */

.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

@media (prefers-reduced-motion: reduce) {
  .ball-shake,
  .ball,
  .face-8,
  .face,
  .readout .answer,
  .readout .meta,
  .readout .breakdown,
  .readout .failure,
  .meter-fill,
  .diverge-fill {
    animation: none !important;
    transition: none !important;
  }
  .ball-shake.is-shaking,
  .ball-shake.is-rocking { animation: none !important; }
}

@media (max-height: 560px) {
  .hint { display: none; }
  .readout { min-height: 0; }
}
