/* ==========================================================================
   components.css — cup, orbs, tray, choices, liquid slider, radar, keypad.
   Logical properties only: never left / right.
   ========================================================================== */

.icon-defs { position: absolute; inline-size: 0; block-size: 0; }

/* --- language toggle: the only control on screen 1 ---------------------- */

.lang-toggle {
  position: absolute;
  inset-block-end: var(--space-4);
  inset-inline-start: var(--space-4);
  min-inline-size: var(--tap-min);
  min-block-size: 3rem;
  padding-inline: var(--space-3);
  border: var(--hairline-width) solid var(--hairline);
  border-radius: var(--radius-pill);
  color: var(--olive-mid);
  font-size: var(--step--1);
  font-weight: 500;
  transition: border-color var(--dur-tap) var(--ease-out),
              color var(--dur-tap) var(--ease-out);
  z-index: 5;
}

.lang-toggle:active { border-color: var(--accent); color: var(--accent); }

/* --- hidden 5-tap admin target ----------------------------------------- */

.admin-corner {
  position: fixed;
  inset-block-start: 0;
  inset-inline-start: 0;
  inline-size: 5rem;
  block-size: 5rem;
  z-index: 90;
}

/* --- primary call to action -------------------------------------------- */

.cta {
  justify-self: center;
  min-block-size: var(--tap-min);
  padding-block: var(--space-3);
  padding-inline: var(--space-6);
  border-radius: var(--radius-pill);
  background: var(--accent);
  color: var(--cream);
  font-size: var(--step-1);
  font-weight: 700;
  transition: transform var(--dur-tap) var(--ease-out),
              background-color var(--dur-move) var(--ease-out),
              opacity var(--dur-move) var(--ease-out);
}

.cta:disabled { background: var(--hairline); opacity: 0.7; }
.cta:not(:disabled):active { transform: scale(0.97); }

/* ==========================================================================
   The V60, at the scale of the screen.
   A badge in the corner is a status readout; a brewer the size of the room is
   the thing the customer is filling. It sits behind the questions at low
   contrast so it reads as the space they are in, not as content.
   ========================================================================== */

.rigbg {
  position: absolute;
  inset: 0;
  padding: var(--space-4);
  pointer-events: none;
  z-index: 0;
  /* A watermark, not a frame. It has to be readable as a brew in progress
     without ever competing with the answer the customer is reading. */
  opacity: 0.3;
}

/* Let preserveAspectRatio do the fitting: sizing one axis and leaving the
   other to the aspect ratio pushed it off the bottom of the iPad. */
.rigbg__svg {
  inline-size: 100%;
  block-size: 100%;
}

.rigbg__line,
.rigbg__rib {
  fill: none;
  stroke: var(--hairline);
  stroke-width: 1.4;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.rigbg__rib { stroke-width: 1; }

/* The drip never stops: the kiosk is always mid-brew. */
.rigbg__drip {
  stroke: var(--accent);
  stroke-width: 3;
  stroke-linecap: round;
  stroke-dasharray: 7 40;
  animation: rigbg-drip 1.9s linear infinite;
}

@keyframes rigbg-drip {
  from { stroke-dashoffset: 40; }
  to   { stroke-dashoffset: -7; }
}

/* The coffee in the carafe is what the customer is actually filling. */
.rigbg__liquid {
  fill: var(--accent);
  opacity: 0.34;
  transition: y 1000ms var(--ease-out), fill var(--dur-settle) var(--ease-out);
}

/* Everything the customer answers with sits above the brewer.
   The orb canvas needs naming explicitly: it is `z-index: auto` and comes
   before the brewer in the DOM, so without this the watermark painted
   straight over the answers and every particle burst. */
.deck__canvas { z-index: 1; }

.deck__head,
.deck__stage,
.deck__foot { position: relative; z-index: 2; }

/* ==========================================================================
   Deck choices — the visible sphere is drawn on the canvas beneath; this is
   the hit target, the label, and the space the orb is allowed to occupy.
   ========================================================================== */

/* The symbol's paths carry no fill of their own, so it has to be named here
   rather than inherited: an unset SVG fill is black, not currentColor. */
.brandmark {
  inline-size: clamp(9rem, 22vw, 15rem);
  block-size: auto;
  color: var(--olive);
  fill: currentColor;
}

.choice {
  position: relative;
  display: grid;
  grid-template-rows: minmax(0, 1fr) auto;
  justify-items: center;
  gap: var(--space-2);
  flex: 0 1 var(--choice-basis, 30%);
  /* An exact share of the row, so the art can then take all of it. */
  block-size: calc(
    (100% - (var(--choice-rows, 1) - 1) * var(--gap)) / var(--choice-rows, 1)
  );
  min-block-size: 0;
  padding: 0;
  background: none;
  border: 0;
}

/* Sized from the viewport, not from the row.
   `block-size: 100%` here was circular: the parent centres its rows, so the
   row is content-sized, so the art's 100% resolved against a row whose height
   depended on the art — and everything collapsed to zero. A clamp on the
   shorter axis is deterministic and still scales across every iPad. */
/* Sized from the row it was given, not from a viewport guess: height first,
   then the aspect ratio derives the width. */
.choice__art {
  display: grid;
  place-items: center;
  block-size: 100%;
  aspect-ratio: 1;
  max-inline-size: 100%;
  justify-self: center;
  align-self: center;
  /* A grid box has min-height: auto, which is its content's min-content
     height — and that overrides aspect-ratio. Art with a tall viewBox was
     stretching its own square box until this was pinned to zero. */
  min-block-size: 0;
  min-inline-size: 0;
}

/* Art whose viewBox is taller than it is wide would otherwise resolve to its
   own intrinsic height and stretch the square box that contains it. */
.choice__art > * {
  max-inline-size: 100%;
  max-block-size: 100%;
}

/* The label is what the customer actually reads from a metre away, so it
   carries the weight of a heading rather than a caption. (This rule was lost
   in an earlier refactor, which left the answers at the browser default.) */
.choice__label {
  max-inline-size: 16ch;
  color: var(--ink);
  font-size: clamp(var(--step-1), 3.3vh, var(--step-3));
  font-weight: 700;
  line-height: 1.2;
  text-align: center;
  text-wrap: balance;
  transition: color var(--dur-move) var(--ease-out);
}

.choice.is-chosen .choice__label { color: var(--accent); }

/* ==========================================================================
   Answer art.
   Each answer shows the thing it names. Depth comes from a lit gradient, a
   specular highlight and a contact shadow rather than from a 3D renderer —
   at kiosk distance it reads as solid, and it costs one SVG.
   ========================================================================== */

.art3d {
  inline-size: 100%;
  block-size: 100%;
  overflow: visible;
  /* Lets the pieces tilt in depth without a library. */
  perspective: 420px;
}

.art3d__ground {
  fill: var(--ink);
  opacity: 0.14;
  transition: opacity var(--dur-move) var(--ease-out),
              rx var(--dur-move) var(--ease-out);
}

.art3d__piece {
  transform-origin: 50% 62%;
  rotate: var(--tilt, 0deg);
  transition: translate var(--dur-move) var(--ease-spring),
              rotate var(--dur-move) var(--ease-spring),
              scale var(--dur-move) var(--ease-spring);
}

/* Picking lifts the prop off the ground and the shadow spreads under it. */
.choice.is-chosen .art3d__piece { translate: 0 -5%; scale: 1.07; }
.choice.is-chosen .art3d__ground { opacity: 0.2; rx: 30; }

/* A few props have a life of their own. */
.art3d__drip { animation: drip-fall 2.6s var(--ease-out) infinite; }
.art3d__bloom { animation: bloom-turn 14s linear infinite; transform-origin: 50px 52px; }
.art3d__spin { animation: prop-rock 5s var(--ease-out) infinite; }
.art3d__fall { animation: prop-bob 3.2s var(--ease-out) infinite; }
.art3d__leaf { animation: leaf-wave 4.4s var(--ease-out) infinite; transform-origin: 58px 24px; }

@keyframes drip-fall {
  0%, 100% { opacity: 0; stroke-dasharray: 2 30; stroke-dashoffset: 0; }
  25%      { opacity: 0.9; }
  70%      { opacity: 0.9; stroke-dasharray: 26 30; stroke-dashoffset: -22; }
  85%      { opacity: 0; }
}

@keyframes bloom-turn { to { rotate: 360deg; } }

@keyframes prop-rock {
  0%, 100% { rotate: var(--tilt, 0deg); }
  50%      { rotate: calc(var(--tilt, 0deg) + 14deg); }
}

@keyframes prop-bob {
  0%, 100% { translate: 0 0; }
  50%      { translate: 0 -6%; }
}

@keyframes leaf-wave {
  0%, 100% { rotate: 0deg; }
  50%      { rotate: -9deg; }
}

/* ==========================================================================
   The body control — a volume, not a row.
   Light at the top, heavy at the bottom, and the liquid inside thickens and
   darkens as it rises, so the control is the answer.
   ========================================================================== */

.vol {
  --level: 0;
  display: grid;
  grid-template-rows: auto minmax(0, 1fr) auto;
  justify-items: center;
  gap: var(--space-2);
  /* A definite height, not a percentage: the flex parent's height is
     content-driven, so `min(100%, …)` resolved to auto and collapsed the
     track to its own border. */
  block-size: clamp(13rem, 44vh, 23rem);
}

.vol__nudge {
  display: grid;
  place-items: center;
  inline-size: 3.4rem;
  block-size: 3.4rem;
  border: var(--hairline-width) solid var(--hairline);
  border-radius: 50%;
  color: var(--olive-mid);
  transition: border-color var(--dur-tap) var(--ease-out),
              background-color var(--dur-tap) var(--ease-out),
              color var(--dur-tap) var(--ease-out);
}

.vol__nudge svg {
  inline-size: 1.6rem;
  block-size: 1.6rem;
  fill: none;
  stroke: currentColor;
  stroke-width: 2.4;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.vol__nudge:active {
  border-color: var(--accent);
  background: var(--accent-wash);
  color: var(--accent);
}

.vol__track {
  position: relative;
  inline-size: clamp(5rem, 11vh, 8rem);
  block-size: 100%;
  border: 2px solid var(--hairline);
  border-radius: var(--radius-lg);
  background: var(--cream-sunk);
}

/* The clip lives here so the labels can sit outside the track. */
.vol__well {
  position: absolute;
  inset: 0;
  border-radius: inherit;
  overflow: hidden;
}

/* The fill rises from the bottom and gains body as it goes. */
.vol__fill {
  position: absolute;
  inset-inline: 0;
  inset-block-end: 0;
  block-size: calc(var(--level) * 100%);
  background: linear-gradient(
    to top,
    color-mix(in srgb, var(--tone, var(--accent)) 92%, #000) 0%,
    var(--tone, var(--accent)) 70%,
    color-mix(in srgb, var(--tone, var(--accent)) 70%, #fff) 100%);
  transition: block-size var(--dur-settle) var(--ease-spring),
              background var(--dur-settle) var(--ease-out);
}

/* A moving surface on top of the liquid, so it reads as poured, not printed. */
.vol__fill::after {
  position: absolute;
  inset-block-start: -0.4rem;
  inset-inline: -10%;
  block-size: 0.8rem;
  border-radius: 50%;
  background: color-mix(in srgb, var(--tone, var(--accent)) 60%, #fff);
  content: '';
  animation: vol-slosh 3.4s var(--ease-out) infinite;
}

@keyframes vol-slosh {
  0%, 100% { transform: translateX(-4%) scaleY(1); }
  50%      { transform: translateX(4%) scaleY(1.25); }
}

.vol__stops {
  position: absolute;
  inset: 0;
  display: grid;
  grid-template-rows: repeat(var(--steps, 3), 1fr);
}

.vol__stop {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--ink-soft);
  font-size: var(--step--1);
  font-weight: 500;
  text-align: start;
}

.vol__pip {
  flex: none;
  inline-size: 0.7rem;
  block-size: 0.7rem;
  border: 2px solid var(--olive-mid);
  border-radius: 50%;
  background: var(--cream);
  transition: border-color var(--dur-tap) var(--ease-out),
              scale var(--dur-tap) var(--ease-spring);
}

/* The labels sit outside the track so the liquid never swallows them. */
.vol__label {
  position: absolute;
  inset-inline-start: calc(100% + var(--space-3));
  inline-size: max-content;
  max-inline-size: 11rem;
  color: var(--ink);
  font-size: clamp(var(--step-0), 2.8vh, var(--step-2));
  font-weight: 700;
  white-space: normal;
}

.vol__stop.is-chosen { color: var(--ink); }
.vol__stop.is-chosen .vol__pip { border-color: var(--cream); scale: 1.4; }
.vol__stop.is-chosen .vol__label { color: var(--ink); font-weight: 700; }

/* ==========================================================================
   The mascot — the wordmark's own "e".
   The glyph's crossbar already reads as the crease down a coffee bean, so the
   character is the letter itself with a face, steam off the top and a saucer
   under it. Nothing here is a new drawing; it is the logo, alive.
   ========================================================================== */

.mascot {
  position: relative;
  display: grid;
  justify-items: center;
  inline-size: var(--mascot-size, 4rem);
  flex: none;
  /* A letterform is not direction-dependent. Without this the eyes mirror
     with the page and land outside the bowl in Arabic. */
  direction: ltr;
}

.mascot__body {
  position: relative;
  display: grid;
  place-items: center;
  inline-size: 100%;
  aspect-ratio: 1;
}

.mascot__e {
  inline-size: 100%;
  block-size: 100%;
  fill: var(--accent);
}

/* Dark eyes inside the letter's counter — the enclosed white space above the
   crossbar. Cream eyes were invisible there: the counter is already the page
   colour, so a cream dot on it is nothing at all. */
.mascot__eye {
  position: absolute;
  inline-size: 12%;
  block-size: 12%;
  border-radius: 50%;
  background: var(--ink);
  inset-block-start: 33%;
}

.mascot__eye--a { inset-inline-start: 30%; }
.mascot__eye--b { inset-inline-start: 49%; }

.mascot__saucer {
  inline-size: 72%;
  block-size: 0.3rem;
  border-radius: var(--radius-pill);
  background: color-mix(in srgb, var(--ink) 18%, transparent);
}

/* --- steam -------------------------------------------------------------- */

.mascot__steam {
  position: absolute;
  inset-block-end: 78%;
  inline-size: 62%;
  aspect-ratio: 3 / 2;
}

.mascot__steam svg { inline-size: 100%; block-size: 100%; }

.mascot__steam path {
  fill: none;
  stroke: var(--accent);
  stroke-width: 3.4;
  stroke-linecap: round;
  opacity: 0.35;
  stroke-dasharray: 28 60;
  animation: steam-rise 3.2s linear infinite;
}

.mascot__steam path:nth-child(2) { animation-duration: 4.1s; animation-delay: -1.4s; }
.mascot__steam path:nth-child(3) { animation-duration: 3.6s; animation-delay: -2.4s; }

@keyframes steam-rise {
  from { stroke-dashoffset: 88; }
  to   { stroke-dashoffset: 0; }
}

/* --- idle: it drifts, so the eye finds it without a label ---------------- */

.mascot--small {
  --mascot-size: 3.2rem;
  animation: mascot-drift 5.2s var(--ease-out) infinite;
}

@keyframes mascot-drift {
  0%, 100% { transform: translateY(0) rotate(-3deg); }
  35%      { transform: translateY(-0.42rem) rotate(3deg); }
  70%      { transform: translateY(0.16rem) rotate(-1deg); }
}

.mascot--large { --mascot-size: clamp(5.5rem, 15vh, 9rem); }

/* ==========================================================================
   The call to action — a card, not a link. It is the one optional thing on
   the screen, so it has to look like an offer rather than fine print.
   ========================================================================== */

.oracle-cta {
  display: flex;
  justify-self: start;
  align-items: center;
  gap: var(--space-3);
  margin-block-start: var(--space-3);
  padding-block: var(--space-2);
  padding-inline: var(--space-3) var(--space-4);
  border: 2px solid var(--accent-edge);
  border-radius: var(--radius-lg);
  background: var(--accent-wash);
  text-align: start;
  transition: transform var(--dur-tap) var(--ease-out),
              border-color var(--dur-move) var(--ease-out);
}

.oracle-cta:active { transform: scale(0.98); border-color: var(--accent); }

.oracle-cta__text { display: grid; gap: 0.1rem; }

.oracle-cta__lead {
  color: var(--olive-mid);
  font-size: var(--step--1);
}

.oracle-cta__main {
  color: var(--accent);
  font-size: var(--step-1);
  font-weight: 700;
}

/* ==========================================================================
   "Let error choose for you" — the overlay it opens in.
   ========================================================================== */

.oracle {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  background: color-mix(in srgb, var(--ink) 42%, transparent);
  z-index: 30;
}

.oracle__panel {
  display: grid;
  justify-items: center;
  gap: clamp(var(--space-2), 1.6vh, var(--space-3));
  inline-size: min(32rem, 82%);
  max-block-size: calc(100% - var(--space-5));
  padding: clamp(var(--space-3), 3vh, var(--space-5));
  border-radius: var(--radius-lg);
  background: var(--cream);
  text-align: center;
}

.oracle__say {
  color: var(--olive-mid);
  font-size: clamp(var(--step-0), 2.2vh, var(--step-1));
  font-weight: 500;
}

/* Once it has decided, it stops saying it is deciding. */
.oracle[data-phase='revealing'] .oracle__say,
.oracle[data-phase='revealed'] .oracle__say { display: none; }

/* --- the weighing --------------------------------------------------------- */

.oracle__weighing {
  display: grid;
  gap: var(--space-2);
  inline-size: 100%;
  list-style: none;
}

.oracle__candidate {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1.6fr);
  align-items: center;
  gap: var(--space-3);
  opacity: 0.55;
}

.oracle__candidate-name {
  color: var(--olive-mid);
  font-size: var(--step--1);
  text-align: start;
}

.oracle__bar {
  block-size: 0.55rem;
  border-radius: var(--radius-pill);
  background: var(--cream-sunk);
}

/* The bar grows to the distance the matcher actually computed. */
.oracle__bar::after {
  display: block;
  inline-size: 0%;
  block-size: 100%;
  border-radius: var(--radius-pill);
  background: var(--accent);
  content: '';
  transition: inline-size 900ms var(--ease-out);
  transition-delay: var(--delay, 0ms);
}

.oracle[data-phase] .oracle__bar::after { inline-size: calc(var(--fill, 0.5) * 100%); }

.oracle__candidate.is-picked { opacity: 1; }
.oracle__candidate.is-picked .oracle__candidate-name { color: var(--ink); font-weight: 700; }

/* --- the verdict ----------------------------------------------------------- */

.oracle__verdict {
  display: grid;
  gap: 0.1rem;
  justify-items: center;
  opacity: 0;
  transform: translateY(0.6rem);
  transition: opacity var(--dur-settle) var(--ease-out),
              transform var(--dur-settle) var(--ease-out);
}

.oracle[data-phase='revealing'] .oracle__verdict,
.oracle[data-phase='revealed'] .oracle__verdict { opacity: 1; transform: none; }

.oracle__why { color: var(--olive-mid); font-size: var(--step--1); }

.oracle__name {
  font-size: clamp(var(--step-2), 3.6vh, var(--step-4));
  line-height: 1.08;
}

.oracle__latin {
  color: var(--olive-mid);
  font-size: var(--step--1);
  letter-spacing: 0.03em;
}

.oracle__meta { color: var(--olive-mid); font-size: var(--step--1); }

.oracle__notes {
  max-inline-size: 32ch;
  padding-block-start: var(--space-1);
  font-size: clamp(var(--step--1), 2vh, var(--step-0));
  line-height: 1.45;
}

/* --- the mascot's performance inside the overlay ------------------------- */

/* It arrives rather than appears. */
.oracle[data-phase] .mascot--large {
  animation: mascot-arrive 620ms var(--ease-spring) both;
}

@keyframes mascot-arrive {
  from { opacity: 0; transform: translateY(-2.4rem) rotate(-16deg) scale(0.7); }
  to   { opacity: 1; transform: none; }
}

/* Thinking: it leans side to side, and the steam speeds up like it is working. */
.oracle[data-phase='thinking'] .mascot--large .mascot__body {
  animation: mascot-ponder 1.35s var(--ease-out) infinite;
}

.oracle[data-phase='thinking'] .mascot--large .mascot__steam path {
  animation-duration: 1.5s;
  opacity: 0.6;
}

@keyframes mascot-ponder {
  0%, 100% { transform: rotate(-9deg) translateY(0); }
  50%      { transform: rotate(9deg) translateY(-0.35rem); }
}

/* Deciding: a double take, then a hop. */
.oracle[data-phase='revealing'] .mascot--large .mascot__body,
.oracle[data-phase='revealed'] .mascot--large .mascot__body {
  animation: mascot-eureka 900ms var(--ease-spring);
}

.oracle[data-phase='revealing'] .mascot--large .mascot__steam path,
.oracle[data-phase='revealed'] .mascot--large .mascot__steam path {
  animation-duration: 0.9s;
  opacity: 0.8;
}

@keyframes mascot-eureka {
  0%   { transform: scale(1) rotate(0); }
  18%  { transform: scale(0.86) rotate(-6deg); }      /* the flinch */
  42%  { transform: scale(1.26) rotate(5deg) translateY(-0.7rem); } /* the hop */
  64%  { transform: scale(0.97) rotate(-3deg) translateY(0.1rem); }
  82%  { transform: scale(1.08) rotate(2deg) translateY(-0.2rem); }
  100% { transform: scale(1) rotate(0); }
}

/* The eyes go wide at the moment it decides. */
.oracle[data-phase='revealing'] .mascot--large .mascot__eye,
.oracle[data-phase='revealed'] .mascot--large .mascot__eye {
  animation: eye-pop 900ms var(--ease-spring);
}

@keyframes eye-pop {
  0%, 100% { transform: scale(1); }
  40%      { transform: scale(1.5); }
}

/* Blinking while it thinks, so it reads as alive rather than as a spinner. */
.oracle[data-phase='thinking'] .mascot__eye { animation: mascot-blink 2.8s steps(1) infinite; }

@keyframes mascot-blink {
  0%, 90%, 100% { transform: scaleY(1); }
  94%           { transform: scaleY(0.15); }
}

/* The punchline lands a beat after the hop. */
.oracle__punch {
  color: var(--accent);
  font-size: var(--step-1);
  font-weight: 700;
}

.oracle[data-phase='revealing'] .oracle__punch,
.oracle[data-phase='revealed'] .oracle__punch {
  animation: punch-in 520ms var(--ease-spring) 240ms both;
}

@keyframes punch-in {
  from { opacity: 0; transform: translateY(0.7rem) scale(0.9); }
  to   { opacity: 1; transform: none; }
}

/* The rejected candidate visibly loses: it dims and slides back. */
.oracle__candidate { transition: opacity var(--dur-move) var(--ease-out),
                                 transform var(--dur-move) var(--ease-out); }

.oracle[data-phase='revealing'] .oracle__candidate:not(.is-picked),
.oracle[data-phase='revealed'] .oracle__candidate:not(.is-picked) {
  opacity: 0.3;
  transform: scale(0.96);
}

.oracle[data-phase='revealing'] .oracle__candidate.is-picked,
.oracle[data-phase='revealed'] .oracle__candidate.is-picked {
  transform: scale(1.03);
}

/* --- back: a control, not a footnote ------------------------------------ */

.deck__back {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  min-block-size: var(--tap-min);
  padding-block: var(--space-2);
  padding-inline: var(--space-4);
  border: var(--hairline-width) solid var(--hairline);
  border-radius: var(--radius-pill);
  color: var(--olive-mid);
  font-size: var(--step-0);
  font-weight: 500;
  transition: border-color var(--dur-tap) var(--ease-out),
              color var(--dur-tap) var(--ease-out),
              background-color var(--dur-tap) var(--ease-out);
}

/* A chevron pointing the way the customer came from. Logical borders alone
   are not enough: the same corner has to be rotated the other way once the
   page mirrors, or it ends up pointing at the ceiling. */
.deck__back::before {
  content: '';
  inline-size: 0.55rem;
  block-size: 0.55rem;
  border-block-start: 2px solid currentColor;
  border-inline-start: 2px solid currentColor;
  rotate: -45deg;
}

/* The two borders meet at the top-inline-start corner, so the point sits at
   135deg in LTR and 45deg in RTL; each needs the rotation that lands it flat
   against the reading direction. */
[dir='rtl'] .deck__back::before { rotate: 45deg; }

.deck__back:active {
  border-color: var(--accent);
  background: var(--accent-wash);
  color: var(--accent);
}

/* ==========================================================================
   The machine.
   Bars alone read as a progress meter. A grinder reads as a decision being
   made: the shortlist drops in as beans, the burrs turn, and what comes out
   of the group head is the answer. The bars stay underneath as the honest
   readout of why — the machine is the theatre, not the reasoning.
   ========================================================================== */

.rig { inline-size: min(100%, clamp(8rem, 19vh, 14rem)); }
.rig__svg { inline-size: 100%; block-size: auto; }

.rig__line {
  fill: none;
  stroke: var(--ink);
  stroke-width: 3;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.rig__body { fill: var(--cream-sunk); }

/* --- beans falling into the hopper -------------------------------------- */

.rig__bean { opacity: 0; }

.oracle[data-phase] .rig__bean {
  animation: bean-drop 900ms var(--ease-out) var(--drop-delay, 0ms) both;
}

@keyframes bean-drop {
  0%   { opacity: 0; transform: translateY(-38px) rotate(-40deg); }
  55%  { opacity: 1; transform: translateY(4px) rotate(6deg); }
  75%  { transform: translateY(-2px) rotate(-3deg); }
  100% { opacity: 1; transform: translateY(0) rotate(0); }
}

/* --- the burrs ----------------------------------------------------------- */

.rig__burr {
  fill: none;
  stroke: var(--accent);
  stroke-width: 3;
  stroke-dasharray: 9 7;
  transform-origin: 130px 99px;
}

.rig__burr--inner { stroke-dasharray: 5 5; }

.oracle[data-phase='thinking'] .rig__burr {
  animation: burr-spin 1.05s linear infinite;
}

.oracle[data-phase='thinking'] .rig__burr--inner {
  animation: burr-spin 0.7s linear infinite reverse;
}

@keyframes burr-spin { to { transform: rotate(360deg); } }

/* The whole body judders while it grinds. */
.oracle[data-phase='thinking'] .rig__svg {
  animation: rig-shake 0.16s linear infinite;
}

@keyframes rig-shake {
  0%, 100% { transform: translateX(0); }
  25%      { transform: translateX(-0.9px); }
  75%      { transform: translateX(0.9px); }
}

/* --- the pour ------------------------------------------------------------ */

.rig__stream {
  stroke: var(--accent);
  stroke-width: 4;
  stroke-linecap: round;
  opacity: 0;
}

.oracle[data-phase='revealing'] .rig__stream,
.oracle[data-phase='revealed'] .rig__stream {
  opacity: 0.9;
  animation: stream-run 0.28s linear infinite;
}

@keyframes stream-run {
  from { stroke-dasharray: 5 4; stroke-dashoffset: 9; }
  to   { stroke-dasharray: 5 4; stroke-dashoffset: 0; }
}

.rig__pour {
  fill: var(--accent);
  transition: y 700ms var(--ease-out) 160ms;
}

.oracle[data-phase='revealing'] .rig__pour,
.oracle[data-phase='revealed'] .rig__pour { y: 174; }

/* Once poured, the grinder settles and the burrs stop. */
.oracle[data-phase='revealing'] .rig__burr,
.oracle[data-phase='revealed'] .rig__burr { opacity: 0.4; }

/* ==========================================================================
   SCREEN 4 — the result card
   ========================================================================== */

.badge {
  padding-block: 0.3rem;
  padding-inline: var(--space-3);
  border-radius: var(--radius-pill);
  background: var(--accent);
  color: var(--cream);
  font-size: clamp(var(--step--1), 2.1vh, var(--step-1));
  font-weight: 700;
}

/* --- radar --------------------------------------------------------------- */

/* Bounded by height, with the width following. `block-size: 100%` looked
   right but resolved against an indefinite row, so the SVG fell back to its
   own intrinsic size and grew to 418px — taller than the space it had. */
.radar {
  display: grid;
  place-items: center;
  inline-size: 100%;
  min-block-size: 0;
}

.radar__svg {
  block-size: clamp(7.5rem, 27vh, 18rem);
  inline-size: auto;
  max-inline-size: 100%;
  overflow: visible;
}

.radar__grid polygon,
.radar__grid line {
  fill: none;
  stroke: var(--hairline);
  stroke-width: 1.4;
}

/* The outermost ring is the scale, so it reads a step stronger. */
.radar__grid polygon:nth-of-type(4) { stroke-width: 2.2; }

.radar__plot {
  fill: var(--accent);
  fill-opacity: 0.3;
  stroke: var(--accent);
  stroke-width: 3.4;
  stroke-linejoin: round;
  transition: points var(--dur-settle) var(--ease-out);
}

.radar__dots circle {
  fill: var(--accent);
  stroke: var(--cream);
  stroke-width: 2.4;
  r: 6;
}

.radar__label {
  fill: var(--ink-soft);
  font-family: var(--font-sans);
  font-size: 15px;
  font-weight: 700;
}

/* --- alternates, laid out across --------------------------------------- */

/* One line: label, the two runners-up, and the offer. Stacking the label on
   its own row cost 40px the triangle needed. */
.alts {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: var(--space-2) var(--space-3);
  inline-size: 100%;
}

.alts__label {
  color: var(--olive-mid);
  font-size: var(--step--1);
}

.alts__list {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  list-style: none;
}

.alt {
  display: grid;
  gap: 0.1rem;
  padding-block: var(--space-2);
  padding-inline: var(--space-3);
  border: var(--hairline-width) solid var(--hairline);
  border-radius: var(--radius-md);
  text-align: center;
}

.alt__name { font-size: clamp(var(--step--1), 2vh, var(--step-1)); font-weight: 700; }
.alt__meta { color: var(--olive-mid); font-size: var(--step--1); }

/* ==========================================================================
   Capture — an ordinary field, permanently on screen.
   The custom keypad went away with the QR: on an iPad the system keyboard is
   the control every customer already knows, and it costs no layout height
   until it is actually needed.
   ========================================================================== */

.capture {
  display: grid;
  grid-template-columns: auto auto auto auto;
  align-items: center;
  justify-content: center;
  gap: var(--space-2) clamp(var(--space-3), 2vw, var(--space-5));
  inline-size: 100%;
  padding-block: var(--space-3);
  padding-inline: var(--space-4);
  border-radius: var(--radius-lg);
  background: var(--cream-sunk);
}

.capture__title {
  font-size: clamp(var(--step--1), 2.1vh, var(--step-1));
  font-weight: 700;
  text-align: start;
}

.phone {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding-block: var(--space-1);
  padding-inline: var(--space-3);
  border: 2px solid var(--hairline);
  border-radius: var(--radius-pill);
  background: var(--cream);
  direction: ltr;
  transition: border-color var(--dur-tap) var(--ease-out);
}

.phone:focus-within { border-color: var(--accent); }

.phone__code {
  color: var(--olive-mid);
  font-size: clamp(var(--step--1), 2vh, var(--step-1));
  font-weight: 500;
}

.phone__input {
  inline-size: 9ch;
  min-block-size: 2.6rem;
  border: 0;
  background: none;
  color: var(--ink);
  font-size: clamp(var(--step-0), 2.6vh, var(--step-2));
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.08em;
  /* The kiosk blocks selection everywhere else; a field has to allow it. */
  -webkit-user-select: text;
  user-select: text;
}

.phone__input:focus { outline: none; }
.phone__input::placeholder { color: var(--hairline); letter-spacing: 0.08em; }

/* --- consent gates send ---------------------------------------------------- */

.consent {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  cursor: inherit;
}

.consent__box {
  position: absolute;
  inline-size: 1px;
  block-size: 1px;
  opacity: 0;
}

.consent__mark {
  display: grid;
  place-items: center;
  flex: none;
  inline-size: 1.6rem;
  block-size: 1.6rem;
  border: 2px solid var(--olive-mid);
  border-radius: var(--radius-sm);
  transition: background-color var(--dur-tap) var(--ease-out),
              border-color var(--dur-tap) var(--ease-out);
}

.consent__box:checked + .consent__mark {
  border-color: var(--accent);
  background: var(--accent);
}

/* Centred by the grid rather than by insets, so the tick does not shift when
   the layout mirrors. */
.consent__box:checked + .consent__mark::after {
  translate: 0 -0.12rem;
  inline-size: 0.38rem;
  block-size: 0.8rem;
  border: solid var(--cream);
  border-width: 0 3px 3px 0;
  content: '';
  rotate: 45deg;
}

.consent__box:focus-visible + .consent__mark { outline: 3px solid var(--accent); }

.consent__text {
  max-inline-size: 24ch;
  color: var(--ink);
  font-size: var(--step--1);
  line-height: 1.3;
  text-align: start;
}

.capture__thanks {
  grid-column: 1 / -1;
  color: var(--accent);
  font-size: clamp(var(--step-0), 2.6vh, var(--step-2));
  font-weight: 700;
  text-align: center;
}

/* Once the number is in, the form gets out of the way. */
.capture[data-state='sent'] > .capture__title,
.capture[data-state='sent'] > .phone,
.capture[data-state='sent'] > .consent,
.capture[data-state='sent'] > .cta { display: none; }

/* ==========================================================================
   Technician panel — five taps in the corner, never advertised.
   ========================================================================== */

.admin {
  position: fixed;
  inset: 0;
  display: grid;
  place-items: center;
  background: color-mix(in srgb, var(--ink) 55%, transparent);
  z-index: 95;
}

.admin__panel {
  display: grid;
  gap: var(--space-4);
  inline-size: min(30rem, 86%);
  padding: var(--space-5);
  border-radius: var(--radius-lg);
  background: var(--cream);
}

.admin__title { font-size: var(--step-2); }

.admin__stats {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: var(--space-2) var(--space-3);
  font-size: var(--step-0);
}

.admin__stats dt { color: var(--olive-mid); }

.admin__stats dd {
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  direction: ltr;
  text-align: start;
  overflow-wrap: anywhere;
}

.admin__actions { display: grid; justify-items: center; gap: var(--space-1); }
